Date range in date range

Actually this task seemed very easy to me, but i got a little bit stuck and would be thankful for some hints :D

I have some events with a start and an end time - and i would like to create a table with calendar weeks.

Therefore i wrote a method to to check if an event is within this week to color it like this:

enter image description here

private boolean inWeek(Date date, Entry pe) {
    return ((pe.getStartsAt().after(Util.firstDayOfWeek(date)) || pe.getStartsAt().equals(Util.firstDayOfWeek(date)))
     && (pe.getEndsAt().before(Util.lastDayOfWeek(date)) || pe.getEndsAt().equals(Util.lastDayOfWeek(date))));
}

This case was okay if events are just lasting one week. but what if the event starts before this week, or ends after this week or even lasts several weeks?

it became very complicated and my current solution was this:

private boolean inWeek(Date date, Entry pe) {

    return  (  pe.getStartsAt().after(Util.firstDayOfWeek(date)) &&  pe.getEndsAt().after(Util.firstDayOfWeek(date)) && pe.getEndsAt().before(Util.lastDayOfWeek(date))    ) 
    ||      (  pe.getStartsAt().before(Util.lastDayOfWeek(date)) &&  pe.getStartsAt().after(Util.firstDayOfWeek(date)) &&  pe.getEndsAt().after(Util.lastDayOfWeek(date))  )
    ||      (  pe.getStartsAt().after(Util.firstDayOfWeek(date)) &&  pe.getEndsAt().before(Util.lastDayOfWeek(date))  )
    ||      (  pe.getStartsAt().before(Util.firstDayOfWeek(date)) && pe.getEndsAt().after(Util.lastDayOfWeek(date)) );

}

but thas still not showing the right coloration in some cells. Does anybody have any hints for me?

(...without proposing joda times ^^)


Having spent my fair share of time mucking around with ... well, time...I can tell you that I'd prefer to let someone else do the work for me.

To that end, if you're will to give it a go, I'd take a look at JodaTime

Basically, what this example does it creates a series of Intervals. One is the "period", or week of year (starting at Monday and finishing on Sunday).

One Interval is an overlapping interval, which spans one week before and one week after the "period", the other is a single day Interval within the "period"

import org.joda.time.Interval;
import org.joda.time.MutableDateTime;
import org.joda.time.Period;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.DateTimeParser;

public class TestTimeInterval {

    public static void main(String[] args) {

        DateTimeFormatter formatter = new DateTimeFormatterBuilder().
                        appendDayOfMonth(2).appendLiteral(" ").
                        appendDayOfWeekText().appendLiteral(" ").
                        appendMonthOfYearText().appendLiteral(" ").
                        appendYear(2, 2).
                        toFormatter();

        // Last week
        MutableDateTime targetStart = MutableDateTime.now();
        targetStart.setDayOfWeek(1);
        targetStart.addDays(-6);

        // Next week
        MutableDateTime targetEnd = MutableDateTime.now();
        targetEnd.setDayOfWeek(7);
        targetEnd.addDays(7);

        System.out.println("Target range = " + formatter.print(targetStart) + " to " + formatter.print(targetEnd));
        Interval targetInterval = new Interval(targetStart, targetEnd);

        // This week
        MutableDateTime start = MutableDateTime.now();
        start.setDayOfWeek(1);

        MutableDateTime end = MutableDateTime.now();
        end.setDayOfWeek(7);

        Interval interval = new Interval(start, end);

        System.out.println("Interval range = " + formatter.print(start) + " to " + formatter.print(end));
        System.out.println("Contains interval = " + targetInterval.contains(interval));

        // Last week
        targetStart = DateTime.now();

        // Next week
        targetEnd = DateTime.now();

        System.out.println("Target range = " + formatter.print(targetStart) + " to " + formatter.print(targetEnd));
        targetInterval = new Interval(targetStart, targetEnd);
        System.out.println("Contains interval = " + interval.contains(targetInterval));
    }

}

Which outputs...

Target range = 10 Tuesday December 2013 to 29 Sunday December 2013
Period range = 16 Monday December 2013 to 22 Sunday December 2013
Contains period = true
Target range = 19 Thursday December 2013 to 19 Thursday December 2013
Contains period = true

What you end up with only need to check the period interval in two ways.

  1. To check if the "period" is within the supplied Interval and
  2. If the supplied Interval is within the "period"...

For example...

 if (period.contains(interval) || interval.contains(period)) {
     // Match...
 }

Now, there is a whole lot of other things to consider, like, if time is not important to the Intervals, you'll want to zero the time (the start the period should be midnight/morning and the end should midnight evening) so you maximums the catch area

Updated making better use of the JodaTime libraries

@BasilBourque was able to highlight some issues with the original example, which I've updated and tested accordingly. Thanks @BasilBourque

While simular to the original, it makes better use the JodaTime libraries

public static void newWay() {

    DateTimeFormatter formatter = new DateTimeFormatterBuilder().
            appendDayOfMonth(2).appendLiteral(" ").
            appendDayOfWeekText().appendLiteral(" ").
            appendMonthOfYearText().appendLiteral(" ").
            appendYear(2, 2).
            toFormatter();

    // Last week
    DateTime targetStart = DateTime.now(DateTimeZone.getDefault()).
            withDayOfWeek(DateTimeConstants.MONDAY).
            minusDays(6);
    //MutableDateTime targetStart = MutableDateTime.now();
    //targetStart.setDayOfWeek(1);
    //targetStart.addDays(-6);

    // Next week
    DateTime targetEnd = DateTime.now(DateTimeZone.getDefault()).
            withDayOfWeek(DateTimeConstants.SUNDAY).
            plusDays(7);
    //MutableDateTime targetEnd = MutableDateTime.now();
    //targetEnd.setDayOfWeek(7);
    //targetEnd.addDays(7);

    System.out.println("Target range = " + formatter.print(targetStart) + " to " + formatter.print(targetEnd));
    Interval targetInterval = new Interval(targetStart, targetEnd);

    // This week
    DateTime start = DateTime.now(DateTimeZone.getDefault()).
            withDayOfWeek(DateTimeConstants.MONDAY);
    //MutableDateTime start = MutableDateTime.now();
    //start.setDayOfWeek(1);

    DateTime end = DateTime.now(DateTimeZone.getDefault()).
            withDayOfWeek(DateTimeConstants.SUNDAY);
    //MutableDateTime end = MutableDateTime.now();
    //end.setDayOfWeek(7);

    Interval interval = new Interval(start, end);

    System.out.println("Period range = " + formatter.print(start) + " to " + formatter.print(end));

    System.out.println("Contains period = " + targetInterval.contains(interval));

    // Last week
    targetStart = DateTime.now();

    // Next week
    targetEnd = DateTime.now();

    System.out.println("Target range = " + formatter.print(targetStart) + " to " + formatter.print(targetEnd));
    targetInterval = new Interval(targetStart, targetEnd);
    System.out.println("Contains period = " + interval.contains(targetInterval));

}

Joda-Time

The Joda-Time 2.3 library makes this work much easier. It includes an Interval class with an overlap method.

See the answer by MadProgrammer for similar code and discussion.

Key Points

The Interval class is smart, considering the beginning of interval to be inclusive and the ending exclusive. You should make comparisons based on the logic of EQUAL TO OR GREATER THAN the start but LESS THAN the stop. Why? Because the moment before the new day is infinitely divisible. You may think, "Well java.util.Date & Joda-Time resolve to milliseconds so I'll use .999". But then you'll be surprised when you port code to Java 8's new java.time.* classes where time resolves to nanoseconds.

Timeline showing ( >= start of day 1 ) and ( < start of day 8 )

To support this comparison, notice that the target week is defined with a call to withTimeAtStartOfDay. Use this method rather than trying to create a midnight by setting zero time elements. This method is smart and handles Daylight Saving Time and other anomalies where there may not be a 00:00:00 midnight time on certain days in certain time zones.

Specify a time zone rather than rely on defaults. All the code in other answers fail to address the time zone. That means they use the default time zone of the JVM. As a consequence, this app gets different results when deployed to other machines set to other time zones. Use proper time zone names, never 3-letter codes.

If your app applies to people and places across time zones, you should consider basing the target week on UTC/GMT (no time zone offset). Notice that is what StackOverflow does in tracking your activity day by day. A "day" is defined by UTC/GMT.

These points are evidence why you should not roll your own date-time logic. Use a competent library instead. In Java, that means either Joda-Time or the new java.time.* classes in Java 8 (inspired by Joda-Time).

ISO Week

By the way, the ISO 8601 standard defines a "week" precisely. More companies and industries in various countries are adopting this standard. Following that standard may prove useful. A Joda-Time DateTime instance knows its ISO week number. Call myDateTime.weekOfWeakYear().get().

Example Code

DateTimeZone timeZone = DateTimeZone.forID( "Europe/Berlin" );

Interval weekInQuestion = new Interval( new DateTime( 2014, 1, 20, 3, 4, 5, timeZone ).withTimeAtStartOfDay(), new DateTime( 2014, 1, 27, 3, 4, 5, timeZone ).withTimeAtStartOfDay() );

Interval i1 = new Interval( new DateTime( 2014, 1, 2, 3, 4, 5, timeZone ), new DateTime( 2014, 1, 3, 23, 4, 5, timeZone ) );
Interval i2 = new Interval( new DateTime( 2014, 1, 24, 3, 4, 5, timeZone ), new DateTime( 2014, 1, 26, 23, 59, 59, timeZone ) );
Interval i3 = new Interval( new DateTime( 2014, 1, 6, 3, 4, 5, timeZone ), new DateTime( 2014, 1, 30, 3, 4, 5, timeZone ) );

boolean i1HitsWeekInQuestion = i1.overlaps( weekInQuestion );
boolean i2HitsWeekInQuestion = i2.overlaps( weekInQuestion );
boolean i3HitsWeekInQuestion = i3.overlaps( weekInQuestion );

Dump to console…

System.out.println( "weekInQuestion: " + weekInQuestion );
System.out.println( "i1: " + i1 + " hits week: " + i1HitsWeekInQuestion );
System.out.println( "i2: " + i2 + " hits week: " + i2HitsWeekInQuestion );
System.out.println( "i3: " + i3 + " hits week: " + i3HitsWeekInQuestion );

When run…

weekInQuestion: 2014-01-20T00:00:00.000+01:00/2014-01-27T00:00:00.000+01:00
i1: 2014-01-02T03:04:05.000+01:00/2014-01-03T23:04:05.000+01:00 hits week: false
i2: 2014-01-24T03:04:05.000+01:00/2014-01-26T23:59:59.000+01:00 hits week: true
i3: 2014-01-06T03:04:05.000+01:00/2014-01-30T03:04:05.000+01:00 hits week: true