Java – Mutually restricting begin and end date-times using p:calendar (no validation)

ajaxcalendarjavajsfprimefaces

We have a requirement to present two p:calendar components to the user, representing a start and end date each. Both datetimes have dates, hours and minutes.
PrimeFaces has perfect mindate, maxdate, minHour, maxHour, minMinute, and minMinute attributes available.

The requirement now is:

It is impossible to set the start datetime to anything greater than or equal to the end datetime.
It is impossible to set the end datetime to anything less than or equal to the end datetime.

The following equation should hold true:

begin datetime < end datetime

Now we tried the following JSF:

<p:calendar id="begin-date"
            value="#{debugManager.selectedBeginDate}"
            mindate="#{debugManager.minBeginDate}"
            maxdate="#{debugManager.maxBeginDate}"
            maxHour="#{debugManager.maxBeginHour}"
            maxMinute="#{debugManager.maxBeginMinute}"
            pattern="yyyy-MM-dd HH:mm"
            showButtonPanel="true"
            readonlyInput="true"
            navigator="true"
            showOn="button"
            required="true">
    <p:ajax event="dateSelect" update="end-date" />
</p:calendar>

<p:calendar id="end-date"
            value="#{debugManager.selectedEndDate}"
            mindate="#{debugManager.minEndDate}"
            minHour="#{debugManager.minEndHour}"
            minMinute="#{debugManager.minEndMinute}"
            pattern="yyyy-MM-dd HH:mm"
            showButtonPanel="true"
            readonlyInput="true"
            navigator="true"
            showOn="button">
    <p:ajax event="dateSelect" update="begin-date" />
</p:calendar>

Here's an examplary min/max method (mindate of end-date):

public Date getMinEndDate()
{
    return this.getSelectedBeginDate();
}

As you can see, the minimum end date is the currently AJAX-selected begin date. Setting an end date correctly disallows setting the begin date past the end date.

The problems start when involving the time into the equation…

Since the interface of p:calendar has separate methods, the bean has to provide the logic:

public int getMinEndHour()
{
    Date selectedBeginDate = this.getSelectedBeginDate();
    Date selectedEndDate = this.getSelectedEndDate();

    if ( selectedBeginDate != null && DateUtil.isSameDay( selectedBeginDate, selectedEndDate ) )
    {
        return DateUtil.getHourOf( selectedBeginDate );
    }

    return ComplianceConstants.DEFAULT_COMPLIANCE_CASE_MIN_END_HOUR;
}

This basically only says if a begin date has been set and it the begin and end dates are currently the same, restrict the selectable end hour (minHour of end-date) to the begin hour.

Operations:

Set the begin datetime to 2013-04-20 12:34 (legit)
Set the end   datetime to 2013-04-22 00:00 (legit)

Now the time for end date sits on 00:00 and selecting a calendar date 2013-04-20 should be allowed as long as the end time is somehow adjusted to at least 12:35.

The p:calendar component however cannot know this and now

sets the end datetime to 2013-04-20 00:00 (legit, but false)

The problem now is that when the user presses a certain new end date in the calendar, the mindate/maxdate attributes cannot restrict the user to hit the the same as the begin date. If the end date time now happens to be before the same begin date's time there's nothing we can do about it (which is wrong).

The followup problem now is that the user is able to close the calendar and just press the submit button to insert false data into the DB. Of course, a validator could/should be run, but we have to somehow achieve this without a validator.

What we were trying next was to patch the setSelectedBeginDate( Date selectedBeginDate ) and setSelectedEndDate( Date selectedEndDate ) methods to adjust the set java.util.Date time portions if the dates were on the same day. Something like this:

public void adjustSelectedEndDate()
{
    if ( this.selectedEndDate != null )
    {
        this.log.infov( "adjustSelectedEndDate: b-hour = {0}, e-hour = {1}", DateUtil.getHourOf( this.selectedBeginDate ), DateUtil.getHourOf( this.selectedEndDate ) );

        if ( DateUtil.isSameDay( this.selectedBeginDate, this.selectedEndDate ) &&
            ( DateUtil.getHourOf( this.selectedEndDate ) < DateUtil.getHourOf( this.selectedBeginDate ) ) ||
              DateUtil.getHourOf( this.selectedEndDate ) == DateUtil.getHourOf( this.selectedBeginDate ) && DateUtil.getMinuteOf( this.selectedEndDate ) <= DateUtil.getMinuteOf( this.selectedBeginDate ) )
        {
            this.log.info( "Adjusting selected end date!" );

            this.selectedEndDate = DateUtil.addOneMinuteTo( DateUtil.copyTime( this.selectedBeginDate, this.selectedEndDate ) );
        }
    }
}

This required us to add @this to the update attribute of each p:calendar so that the respective getters (getSelectedBeginDate() and getSelectedEndDate + the min/max limiters) will be called during update.

Placing an @this on the update however confuses the p:calendar components, making the time sliders only slidable once. Subsequent slider events are simply ignored, behaving broken.

Q's

  • How do you generally approach solving this?
  • Is using p:remoteCommand the way to achieve what we want?

Optional Q:

  • Why hasn't the PrimeFaces p:calendar been implemented to provide a single minDateTime and maxDateTime, which could potentially solve the problems at hand?

I bet this scenario I described has already been solved before. I'd very much appreciate if you could describe the approach you managed to solve this (or even share a partly solution).

Best Answer

Preface:

I don't work with JSF, but there are a couple of things that might steer you back to where you want to be:

a) when working with just the date portion of a dateTime in a standard calendar, consider using:

someCalendar.set(Calendar.MILLISECOND, 0)

b) consider using joda-time, as it seems to be frequently recommended (here, here , and many other places) over the standard library for correctness, performance, and ease of use in many situations.

c) Make sure your bean scope is surviving each ajax call (not redirecting, only sending standard post-backs, etc) and each event handler is getting the faces context (eg. FacesContext facesContext = FacesContext.getCurrentInstance();)

d) mindate and the like probably don't work like you expect , and I don't expect that automatic behavior can be quite so easily interjected.

When those options aren't available, and you have to do it all yourself with what you have:

Philisophical / UX: The first thing I would do is remove the expectation of arrangement or perspective from the pair of dates. Don't treat the pair as a vector that exposes or expects a direction on the timeline.

  • In other words, is a start or from date always less than or earlier than an end or to date? No, as can be seen for a query of historical data, or for applying corrections to events that have either yet to happen or have already happened?

    This connotation can easily confuse a user as to whether they are going 'back to' or 'forward from' (and can easily confuse yourself). Instead I would treat a pair of dates with a time-period between them as just and simply that a pair of dates or a range or a period that declares an interval, and infer their relative position on the timeline depending on the any consequently chosen values. In this way you can honor the respective and inherent requirements that the dates never be equal, and the left is always to the left, the right always to the right.

We can't infer what 'start' or 'from' means, but we can infer some meaning and relative relationship: a right, a left, and a between on a chronological timeline. Note: Always resolve dates to UTC before doing any calculation or comparison.

long oneDateValue = oneDate.toUtc().toMilliseconds();
long anotherDateValue = anotherDate.toUtc().toMilliseconds();

long right = max (oneDateValue, anotherDateValue);
long left = min (oneDateValue, anotherDateValue);

Evaluating Precision: The second thing I would look at when working with a range of dates in any language is similar to how you might deal with floating point numbers. For comparisons, do not compare for equality, but instead compare the delta to an "acceptable error level". In other words, the application is really only concerned with a certain degree of precision, so make sure that only that precision is captured and considered:

const int dateTimeResolutionInMs = 86400000; // milliseconds per day

public bool areEssentiallySame(long left, long right) {

   // the difference between right and left is less than our precision 
   // requires, thus dates are effectively the same
   return (right - left < dateTimeResolutionInMs);
}

Coercing Precision: Thirdly, how do we resolve the difference in values even if within the range of the resolution? (Out application was given more precision than it can handle or expect or needs).

long diff = value % dateTimeResolutionInMs;

  1. Truncate: return value - diff;

  2. Nearest (w/bias): return value + (diff < dateTimeResolutionInMs/ 2) ? -1 * diff : dateTimeResolutionInMs - diff;

  3. Others: there are lots of other strategies for either shrinking or expanding a value to a preferred resolution or precision

Addendum: As far as getting post-backs/Ajax calls to return a view with the values you expect for the events fired by a calendar element, you may want to separate that concern off to a new question if the note in the preface didn't get you anywhere, and you know for certain your bean is properly registered and recognized. You may have some browser/browser-version specific issues that contribute to the undesired behavior, and like anything else, there are issues, both known and unknown.

Related Topic