3

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 ^^)

Taylan Aydinli
  • 4,333
  • 15
  • 39
  • 33
Audrey Delany
  • 113
  • 4
  • 12
  • what is the date parameter supposed to mean that you are passing in? – Oliver Watkins Dec 19 '13 at 09:24
  • these are just simple java date objects. Each "Entry" has an date object as start date and end date and the "Util"-class returns date objects for an input date to deliver monday and sunday (in my case for each cell). – Audrey Delany Dec 19 '13 at 09:32
  • why don't you want to use joda? – bobah Dec 19 '13 at 09:36
  • That's the only part of code i am working with date objects. Don't you agree that a complete library is an overkill for just one - actually simple - logical method? – Audrey Delany Dec 19 '13 at 09:40
  • @AudreyDelany No, adding [Joda-Time](http://www.joda.org/joda-time/) is not overkill. The first thing I do to any new project is add Joda-Time. I use it immediately for logging out to command-line during development & testing, for example. For your purpose, Joda-Time contains classes aimed directly at your problem. As for "one simple method", dealing with date-time is *never* simple! – Basil Bourque Jan 22 '14 at 22:52
  • @AudreyDelany (a) Are those week numbers in the column headers of your chart? (b) Is each row representing a single task, with the green meaning at least one or more days of that week are included in the task's time span? – Basil Bourque Jan 23 '14 at 01:54

4 Answers4

2

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));

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    The way to zero the time is to call [`withTimeAtStartOfDay`](http://www.joda.org/joda-time/apidocs/org/joda/time/DateTime.html#withTimeAtStartOfDay()). The [`contains`](http://www.joda.org/joda-time/apidocs/org/joda/time/ReadableInterval.html#contains(org.joda.time.ReadableInstant)) is smart about this "catch area" as you call it, because (see doc) `duration intervals are inclusive of the start instant and exclusive of the end`. – Basil Bourque Jan 22 '14 at 23:05
  • 1
    No need to use the mutable versions of the Joda-Time classes. You just open yourself needlessly to thread-safety issues. The designers intended us to generally use the immutable classes. The relevant classes return fresh DateTime (etc.) instances, so you can still write ["fluent" code](http://en.wikipedia.org/wiki/Fluent_interface) with call-chaining. – Basil Bourque Jan 22 '14 at 23:07
  • @BasilBourque The initial use of `MutableDateTime` was more for convenience as I had no other references. If I had a `Date` or other concrete point in time to work with, I would have preferred to use those, but coming from a `Calendar` background, this made the most sense at the time. I like the link to `withTimeAtStartOfDay`, this is good to know, thanks. Most of the answer revolves around the "Contains Period" determination, the rest is filler to make data for these examples... – MadProgrammer Jan 22 '14 at 23:30
  • @BasilBourque Which leads to the question of - if you don't recommend `MutableDateTime`, what would you recommend instead? Based on a single point in time, how would you recommend making a `DateTime` which is one week before and after that piviot, for example? – MadProgrammer Jan 22 '14 at 23:32
  • 1
    The `withTimeAtStartOfDay` method is newer. It supplants the older "midnight"-related methods which are no longer recommended by the Joda-Time folks. – Basil Bourque Jan 23 '14 at 01:44
  • 1
    To get a week ago: `DateTime dateTimeLastWeek = new DateTime( DateTimeZone.forID( "Europe/Berlin" ).minusWeeks( 1 );`, or call `.minusDays( 6 );` as need be. Add a call to `withTimeAtStartOfDay` onto the end if you want the first moment of the day. Note that I specified a time zone, a crucial element missing from the question and all the answers. – Basil Bourque Jan 23 '14 at 01:56
  • @BasilBourque All very nice to know. Given the lack of requirement from question (about time zones) probably accounts for the lack in the answers. I guess we assume default timezone is suitable for the given requirements, but still a decent comment! Don't know why I missed the `plus` and `minus` methods... – MadProgrammer Jan 23 '14 at 02:01
  • 1
    Call [`withDayOfWeek`](http://www.joda.org/joda-time/apidocs/org/joda/time/DateTime.html#withDayOfWeek(int)) to get a fresh DateTime instance for a particular day. [DateTimeConstants](http://www.joda.org/joda-time/apidocs/org/joda/time/DateTimeConstants.html) defines some constants in English for each day of the week, such as `DateTimeConstants.MONDAY`. Yes, Joda-Time has lots of goodies. I keep discovering more as I go spelunking. – Basil Bourque Jan 23 '14 at 02:03
  • @BasilBourque `DateTimeConstants`!! That's where those things are! Thanks for the time, I've updated the answer to included your suggestions and recommendations, hope it's more in line with how JodaTime should be used (still kinda new to it all) – MadProgrammer Jan 23 '14 at 02:36
  • Probably don't need the formatter. Try: `System.out.println("Interval = " + interval );`. And use the word "Interval" in text if using "Interval" in code, to avoid confusion with Joda-Time’s "Period" class. – Basil Bourque Jan 23 '14 at 02:42
  • @BasilBourque Wow, you're picky - I like ;). While `System.out.println("Period range = " + interval );` I think the formatter is a little pretty and it demonstrates it's use, but that's just me ;) – MadProgrammer Jan 23 '14 at 02:45
  • Actually, I wasn't being picky as much as I was pointing out that the Interval class has a smart [`toString` implementation](http://www.joda.org/joda-time/apidocs/org/joda/time/ReadableInterval.html#toString%28%29), in case you were not aware. The Interval prints as a standard [ISO 8601 "interval" format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `/` – Basil Bourque Jan 23 '14 at 03:47
  • @BasilBourque Actually, I was talking about the use of "Interval" over "Period" ;) – MadProgrammer Jan 23 '14 at 03:52
2

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
Community
  • 1
  • 1
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
0

I tried another solution - that seemed to work fine but i am not quite sure

private boolean inWeek(Date date, Entry pe) {
        return  ((pe.getEndsAt().after(Util.firstDayOfWeek(date)) || pe.getEndsAt().equals(Util.firstDayOfWeek(date)) )
            &&  (pe.getStartsAt().before(Util.lastDayOfWeek(date)) || pe.getStartsAt().equals(Util.lastDayOfWeek(date)) ));
}
Audrey Delany
  • 113
  • 4
  • 12
0

I would make the code more readable for a start

    private boolean inWeek(Date date, Entry pe) {

    Date eventStart = pe.getStartsAt();
    Date eventEnd = pe.getEndsAt();
    Date firstDay = Util.firstDayOfWeek(date);
    Date lastDay = Util.lastDayOfWeek(date);

    boolean isInweek =  (eventStart.after(firstDay) && 
                            eventEnd.after(firstDay) && 
                            eventEnd.before(lastDay))
            || 
                        (eventStart.before(lastDay) && 
                            eventStart.after(firstDay) && 
                            eventEnd.after(lastDay))
            || 
                        (eventStart.after(firstDay) && 
                            eventEnd.before(lastDay))
            || 
                        (eventStart.before(firstDay) && 
                            eventEnd.after(lastDay));

    return isInweek;

}

This will probably make the problem more visible.

You have eventEnd.after(lastDay) in the second block. I dont think thats right.

And first block is redundant because the third block would be exactly the same.

Wouldnt just this suffice ?

    private boolean inWeek(Date date, Entry pe) {

    Date eventStart = pe.getStartsAt();
    Date eventEnd = pe.getEndsAt();
    Date firstDay = Util.firstDayOfWeek(date);
    Date lastDay = Util.lastDayOfWeek(date);

    boolean isInweek =  
                        (eventStart.after(firstDay) && //event is INSIDE of the week.
                            eventEnd.before(lastDay))
            || 
                        (eventStart.before(firstDay) && //event is OUTSIDE of the week 
                            eventEnd.after(lastDay));

    return isInweek;

}
Oliver Watkins
  • 12,575
  • 33
  • 119
  • 225
  • In your code the event has to start and end in the specific week or start before the specific week and end after it. What if it starts between firstDay and lastDay but ends the week after the specific week or it begins the week before and ends between firstDay and lastDay. – mylopeda Dec 19 '13 at 10:04