From 30a378ef5b9fc468f628fc3254796ecf4cb565af Mon Sep 17 00:00:00 2001 From: Philip Helger Date: Thu, 28 Jan 2021 17:28:12 +0100 Subject: [PATCH] Extended ILocalDatePeriod; #23 --- .../datetime/period/ILocalDatePeriod.java | 149 +++++++++++++++--- .../datetime/period/LocalDatePeriod.java | 4 +- .../datetime/period/LocalDatePeriodTest.java | 122 ++++++++++++++ 3 files changed, 256 insertions(+), 19 deletions(-) diff --git a/ph-datetime/src/main/java/com/helger/datetime/period/ILocalDatePeriod.java b/ph-datetime/src/main/java/com/helger/datetime/period/ILocalDatePeriod.java index f24f62a33..ec0fd766b 100644 --- a/ph-datetime/src/main/java/com/helger/datetime/period/ILocalDatePeriod.java +++ b/ph-datetime/src/main/java/com/helger/datetime/period/ILocalDatePeriod.java @@ -19,7 +19,9 @@ import java.time.LocalDate; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import com.helger.commons.ValueEnforcer; import com.helger.commons.datetime.PDTFactory; import com.helger.datetime.domain.IHasStartAndEnd; @@ -30,6 +32,64 @@ */ public interface ILocalDatePeriod extends IHasStartAndEnd { + /** + * Check if the provided query date is between start and end. A + * null start means "since forever". A null end + * means "until eternity and beyond". + * + * @param aStart + * Start date. May be null. + * @param bInclStart + * true if "start date" = "query date" should be a match, + * false if not. + * @param aEnd + * End date may be null. + * @param bInclEnd + * true if "end date" = "query date" should be a match, + * false if not. + * @param aQuery + * Date to query whether it is inside or not. May not be + * null. + * @return true if query date time ≥ start date time and ≤ + * end date time + * @since 10.0.0 + */ + static boolean isInside (@Nullable final LocalDate aStart, + final boolean bInclStart, + @Nullable final LocalDate aEnd, + final boolean bInclEnd, + @Nonnull final LocalDate aQuery) + { + ValueEnforcer.notNull (aQuery, "QueryDT"); + + if (aStart != null && (bInclStart ? aQuery.compareTo (aStart) < 0 : aQuery.compareTo (aStart) <= 0)) + return false; + + if (aEnd != null && (bInclEnd ? aQuery.compareTo (aEnd) > 0 : aQuery.compareTo (aEnd) >= 0)) + return false; + + return true; + } + + /** + * Check if the provided date is inside this period, assuming that start and + * end are included in/part of the range. + * + * @param bInclBoundaries + * true if "start date" = "query date" should be a match + * i.e. if "end date" = "query date" should be a match, + * false if this should not be a match. + * @param aDate + * Date to check. May not be null. + * @return true if it is contained, false otherwise. + * @see #isInside(LocalDate, boolean, LocalDate, boolean, LocalDate) + * @since 10.0.0 + */ + default boolean isInPeriod (final boolean bInclBoundaries, @Nonnull final LocalDate aDate) + { + return isInside (getStart (), bInclBoundaries, getEnd (), bInclBoundaries, aDate); + } + /** * Check if the provided date is inside this period, assuming that start and * end are included in/part of the range. @@ -37,20 +97,13 @@ public interface ILocalDatePeriod extends IHasStartAndEnd * @param aDate * Date to check. May not be null. * @return true if it is contained, false otherwise. + * @see #isInPeriod(boolean, LocalDate) * @see #isNowInPeriodIncl() * @since 8.6.5 */ default boolean isInPeriodIncl (@Nonnull final LocalDate aDate) { - final LocalDate aStart = getStart (); - if (aStart != null && aDate.compareTo (aStart) < 0) - return false; - - final LocalDate aEnd = getEnd (); - if (aEnd != null && aDate.compareTo (aEnd) > 0) - return false; - - return true; + return isInPeriod (true, aDate); } /** @@ -59,6 +112,7 @@ default boolean isInPeriodIncl (@Nonnull final LocalDate aDate) * * @return true if the current date is contained, * false otherwise. + * @see #isInPeriod(boolean, LocalDate) * @see #isInPeriodIncl(LocalDate) * @since 8.6.5 */ @@ -74,20 +128,13 @@ default boolean isNowInPeriodIncl () * @param aDate * Date to check. May not be null. * @return true if it is contained, false otherwise. + * @see #isInPeriod(boolean, LocalDate) * @see #isNowInPeriodExcl() * @since 8.6.5 */ default boolean isInPeriodExcl (@Nonnull final LocalDate aDate) { - final LocalDate aStart = getStart (); - if (aStart != null && aDate.compareTo (aStart) <= 0) - return false; - - final LocalDate aEnd = getEnd (); - if (aEnd != null && aDate.compareTo (aEnd) >= 0) - return false; - - return true; + return isInPeriod (false, aDate); } /** @@ -96,6 +143,7 @@ default boolean isInPeriodExcl (@Nonnull final LocalDate aDate) * * @return true if the current date is contained, * false otherwise. + * @see #isInPeriod(boolean, LocalDate) * @see #isInPeriodExcl(LocalDate) * @since 8.6.5 */ @@ -103,4 +151,69 @@ default boolean isNowInPeriodExcl () { return isInPeriodExcl (PDTFactory.getCurrentLocalDate ()); } + + /** + * Check if the provided range 1 has an overlap with the provided range 2. + * + * @param aStart1 + * Start date of the first range. May be null. + * @param aEnd1 + * End date of the first range. May be null meaning it's + * validity is "until eternity and beyond". + * @param aStart2 + * Start date of the second range. May be null. + * @param aEnd2 + * End date of the second range. May be null meaning it's + * validity is "until eternity and beyond". + * @param bInclBoundaries + * true if "start date" = "query date" should be a match + * i.e. if "end date" = "query date" should be a match, + * false if this should not be a match. + * @return true if the 2 ranges have at least one point in time + * (with duration 0) that they share. + * @since 10.0.0 + */ + static boolean hasOverlap (@Nullable final LocalDate aStart1, + @Nullable final LocalDate aEnd1, + @Nullable final LocalDate aStart2, + @Nullable final LocalDate aEnd2, + final boolean bInclBoundaries) + { + if (aStart2 != null && isInside (aStart1, bInclBoundaries, aEnd1, bInclBoundaries, aStart2)) + { + // New start date is in valid range + return true; + } + // New start date is out of range + if (aEnd2 != null && isInside (aStart1, bInclBoundaries, aEnd1, bInclBoundaries, aEnd2)) + { + // New end date is in valid range + return true; + } + + // start1 - end1: 5.1. - 15.1. + // start2 - end2: 1.1. - 31.1. + if (aStart1 != null && isInside (aStart2, bInclBoundaries, aEnd2, bInclBoundaries, aStart1)) + return true; + + if (aEnd1 != null && isInside (aStart2, bInclBoundaries, aEnd2, bInclBoundaries, aEnd1)) + return true; + + return false; + } + + default boolean isOverlappingWith (@Nonnull final ILocalDatePeriod aPeriod, final boolean bInclBoundaries) + { + return hasOverlap (getStart (), getEnd (), aPeriod.getStart (), aPeriod.getEnd (), bInclBoundaries); + } + + default boolean isOverlappingWithIncl (@Nonnull final ILocalDatePeriod aPeriod) + { + return isOverlappingWith (aPeriod, true); + } + + default boolean isOverlappingWithExcl (@Nonnull final ILocalDatePeriod aPeriod) + { + return isOverlappingWith (aPeriod, false); + } } diff --git a/ph-datetime/src/main/java/com/helger/datetime/period/LocalDatePeriod.java b/ph-datetime/src/main/java/com/helger/datetime/period/LocalDatePeriod.java index f806d5519..a6487f9a7 100644 --- a/ph-datetime/src/main/java/com/helger/datetime/period/LocalDatePeriod.java +++ b/ph-datetime/src/main/java/com/helger/datetime/period/LocalDatePeriod.java @@ -19,6 +19,7 @@ import java.time.LocalDate; import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; import com.helger.commons.equals.EqualsHelper; import com.helger.commons.hashcode.HashCodeGenerator; @@ -26,9 +27,10 @@ /** * Default implementation of {@link ILocalDatePeriod}. - * + * * @author Philip Helger */ +@Immutable public class LocalDatePeriod implements ILocalDatePeriod { private final LocalDate m_aStart; diff --git a/ph-datetime/src/test/java/com/helger/datetime/period/LocalDatePeriodTest.java b/ph-datetime/src/test/java/com/helger/datetime/period/LocalDatePeriodTest.java index 0e623f517..24a5bfd4d 100644 --- a/ph-datetime/src/test/java/com/helger/datetime/period/LocalDatePeriodTest.java +++ b/ph-datetime/src/test/java/com/helger/datetime/period/LocalDatePeriodTest.java @@ -92,4 +92,126 @@ public void testBasic () assertFalse (p.isInPeriodExcl (d2)); assertFalse (p.isInPeriodExcl (aNow)); } + + @Test + public void testIsInside () + { + final LocalDate aNow = PDTFactory.getCurrentLocalDate (); + assertTrue (ILocalDatePeriod.isInside (null, true, null, true, aNow)); + assertTrue (ILocalDatePeriod.isInside (null, false, null, false, aNow)); + } + + @Test + public void testDates () + { + final LocalDate aNow = PDTFactory.getCurrentLocalDate (); + final LocalDate dt0 = aNow.minusDays (100); + final LocalDate dt1 = aNow.minusDays (10); + final LocalDate dt2 = aNow.minusDays (2); + final LocalDate dt3 = aNow.plusDays (2); + final LocalDate dt4 = aNow.plusDays (10); + final LocalDate dt5 = aNow.plusDays (100); + + final ILocalDatePeriod r1 = new LocalDatePeriod (dt2, dt3); + assertFalse (r1.isInPeriodIncl (dt0)); + assertFalse (r1.isInPeriodIncl (dt1)); + assertTrue (r1.isInPeriodIncl (dt2)); + assertTrue (r1.isInPeriodIncl (aNow)); + assertTrue (r1.isInPeriodIncl (dt3)); + assertFalse (r1.isInPeriodIncl (dt4)); + assertFalse (r1.isInPeriodIncl (dt5)); + + assertFalse (r1.isInPeriodExcl (dt0)); + assertFalse (r1.isInPeriodExcl (dt1)); + assertFalse (r1.isInPeriodExcl (dt2)); + assertTrue (r1.isInPeriodExcl (aNow)); + assertFalse (r1.isInPeriodExcl (dt3)); + assertFalse (r1.isInPeriodExcl (dt4)); + assertFalse (r1.isInPeriodExcl (dt5)); + + final ILocalDatePeriod r2 = new LocalDatePeriod (dt1, dt4); + assertFalse (r2.isInPeriodIncl (dt0)); + assertTrue (r2.isInPeriodIncl (dt1)); + assertTrue (r2.isInPeriodIncl (dt2)); + assertTrue (r2.isInPeriodIncl (aNow)); + assertTrue (r2.isInPeriodIncl (dt3)); + assertTrue (r2.isInPeriodIncl (dt4)); + assertFalse (r2.isInPeriodIncl (dt5)); + + assertFalse (r2.isInPeriodExcl (dt0)); + assertFalse (r2.isInPeriodExcl (dt1)); + assertTrue (r2.isInPeriodExcl (dt2)); + assertTrue (r2.isInPeriodExcl (aNow)); + assertTrue (r2.isInPeriodExcl (dt3)); + assertFalse (r2.isInPeriodExcl (dt4)); + assertFalse (r2.isInPeriodExcl (dt5)); + + assertTrue (r1.isOverlappingWithIncl (r1)); + assertTrue (r1.isOverlappingWithIncl (r2)); + assertTrue (r2.isOverlappingWithIncl (r1)); + assertTrue (r2.isOverlappingWithIncl (r2)); + + assertFalse (r1.isOverlappingWithExcl (r1)); + assertTrue (r1.isOverlappingWithExcl (r2)); + assertTrue (r2.isOverlappingWithExcl (r1)); + assertFalse (r2.isOverlappingWithExcl (r2)); + + assertFalse (new LocalDatePeriod (dt0, dt1).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertFalse (new LocalDatePeriod (dt0, dt1).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + assertTrue (new LocalDatePeriod (dt0, dt1).isOverlappingWithIncl (new LocalDatePeriod (dt0, dt3))); + assertTrue (new LocalDatePeriod (dt0, dt1).isOverlappingWithExcl (new LocalDatePeriod (dt0, dt3))); + + assertTrue (new LocalDatePeriod (dt0, dt1).isOverlappingWithIncl (new LocalDatePeriod (dt1, dt3))); + assertFalse (new LocalDatePeriod (dt0, dt1).isOverlappingWithExcl (new LocalDatePeriod (dt1, dt3))); + + // Second period is partially open + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithIncl (new LocalDatePeriod (null, dt5))); + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithExcl (new LocalDatePeriod (null, dt5))); + + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithIncl (new LocalDatePeriod (null, dt2))); + assertFalse (new LocalDatePeriod (dt2, dt3).isOverlappingWithExcl (new LocalDatePeriod (null, dt2))); + + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithIncl (new LocalDatePeriod (dt0, null))); + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithExcl (new LocalDatePeriod (dt0, null))); + + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithIncl (new LocalDatePeriod (dt3, null))); + assertFalse (new LocalDatePeriod (dt2, dt3).isOverlappingWithExcl (new LocalDatePeriod (dt3, null))); + + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithIncl (new LocalDatePeriod (null, null))); + assertTrue (new LocalDatePeriod (dt2, dt3).isOverlappingWithExcl (new LocalDatePeriod (null, null))); + + // First period is partially open + assertTrue (new LocalDatePeriod (null, dt5).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertTrue (new LocalDatePeriod (null, dt5).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + assertTrue (new LocalDatePeriod (null, dt2).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertFalse (new LocalDatePeriod (null, dt2).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + assertTrue (new LocalDatePeriod (dt0, null).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertTrue (new LocalDatePeriod (dt0, null).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + assertTrue (new LocalDatePeriod (dt3, null).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertFalse (new LocalDatePeriod (dt3, null).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + assertTrue (new LocalDatePeriod (null, null).isOverlappingWithIncl (new LocalDatePeriod (dt2, dt3))); + assertTrue (new LocalDatePeriod (null, null).isOverlappingWithExcl (new LocalDatePeriod (dt2, dt3))); + + // Both sides partially open + assertTrue (new LocalDatePeriod (dt1, null).isOverlappingWithIncl (new LocalDatePeriod (null, dt3))); + assertTrue (new LocalDatePeriod (dt1, null).isOverlappingWithExcl (new LocalDatePeriod (null, dt3))); + + assertTrue (new LocalDatePeriod (dt1, null).isOverlappingWithIncl (new LocalDatePeriod (null, dt1))); + assertFalse (new LocalDatePeriod (dt1, null).isOverlappingWithExcl (new LocalDatePeriod (null, dt1))); + + assertTrue (new LocalDatePeriod (dt1, null).isOverlappingWithIncl (new LocalDatePeriod (null, null))); + assertTrue (new LocalDatePeriod (dt1, null).isOverlappingWithExcl (new LocalDatePeriod (null, null))); + + assertTrue (new LocalDatePeriod (null, null).isOverlappingWithIncl (new LocalDatePeriod (null, dt1))); + assertTrue (new LocalDatePeriod (null, null).isOverlappingWithExcl (new LocalDatePeriod (null, dt1))); + + // No period, no overlap + assertFalse (new LocalDatePeriod (null, null).isOverlappingWithIncl (new LocalDatePeriod (null, null))); + assertFalse (new LocalDatePeriod (null, null).isOverlappingWithExcl (new LocalDatePeriod (null, null))); + } }