40

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

Kawu
  • 13,647
  • 34
  • 123
  • 195
  • 1
    Answer to your optional Q: Say someone only may choose a datetime between 8 and 17 o'clock (office time), though across multiple days. You'd need to limit the hours instead of the whole datetime. – Menno Apr 23 '13 at 06:40
  • 1
    Good explanation of the problem. AFAIK Omnifaces gives you a hand on this with [``](http://showcase.omnifaces.org/validators/validateOrder). It even includes a sample using `` :). – Luiggi Mendoza Apr 23 '13 at 06:59
  • 2
    Was facing the same problem a while ago, I chose in the end to only control endDate to be greater than beginDate and handle validation for hours and minutes in the ManagedBean if dates are equals. If so, I was showing a dialog bow to the user saying that endDate> beginDate. – javadev Apr 23 '13 at 08:22
  • I recommend to post your problem also in PrimeFaces forums. It sounds as something that should be easily done. If the full date object (time stamp) is there (p:calendar), it should be accessible someway via JavaScript or Java. Regards, – Rodmar Conde May 21 '13 at 09:31
  • Another solution will be to change you UI flow to push the user to enter values in a specific order that makes more easy to you to manage the problem. Like disabling the first calendar after fist date selection, for example. Regards, – Rodmar Conde May 21 '13 at 09:33
  • 2
    One important observation while dealing with `p:calendar`s is that you must ensure that the `value="..."`, `mindate="..."`, `maxdate=""` etc. attributes **never** get any **null** values from the beans that calculate them. This horribly confuses the existing component (it usually falls back to the last known non-null value, looking as if the calendar would cache und not update the component). – Kawu Jun 06 '13 at 08:03
  • I like how this shows the disadvantage of JSF. When you need advanced feature, you're in a dead end. With Wicket, you can do stuff like this in few minutes with few lines of code. – Ondra Žižka Jun 19 '13 at 01:42
  • Note that this is an "advanced" feature of PrimeFaces and not JSF. – Kawu Jun 21 '13 at 06:31
  • Why don't you compare dates with `Date.compareTo();`? – Felype Jul 01 '13 at 10:48

1 Answers1

1

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.

Community
  • 1
  • 1
JustinC
  • 416
  • 5
  • 9