2

I have a Java method as below:

private static boolean isDateBetweenRange(DataSet obj, MyClass dataSource, ConditionContext context) {
    FilterContext fc = dataSource.getData();

    LocalDate dateFieldToCheck = obj.getDate(fc.getDateField()).toInstant()
                                   .atZone(ZoneId.systemDefault()).toLocalDate();
    LocalDate minDate = fc.getMinDateValue();
    LocalDate maxDate = fc.getMaxDateValue();

    if (minDate == null || maxDate == null) {
        minDate = context.getStartDate().toInstant().atZone(ZoneId.systemDefault())
                    .toLocalDate();
        maxDate = context.getEndDate().toInstant().atZone(ZoneId.systemDefault())
                    .toLocalDate();
    }

    boolean result = (dateFieldToCheck.isAfter(minDate) || dateFieldToCheck.isEqual(minDate))
            && (dateFieldToCheck.isBefore(maxDate) || dateFieldToCheck.isEqual(maxDate));

    return result;
}

I want to make the same logic for LocalDateTime also. It's gonna be the exact same code for LocalDateTime if I overload the method.

How can make the method generic to work with LocalDate and LocalDateTime using Generics or any other mechanism?

How can I make context.getXXXDate()... toLocalDate() or toLocalDateTime() a common code based on type I have?

Ajeetkumar
  • 1,271
  • 7
  • 16
  • 34
  • 1
    First I don’t think you can, or at least not completely, though it’s possible to factor out the shared logic. Second, it’s not really clear. Where have you got either a `LocalDate` or a `LocalDateTime` from? Can `fc.getMinDateValue()` return either? If so, what is its declared return type? – Ole V.V. Jun 22 '17 at 09:33
  • 1
    As an aside, there’s a nasty corner case: if someone changes your JVM’s time zone setting while your method runs, you can get surprising results. I suggest you call `ZoneId.systemDefault()` only once and keep the returned zone in a variable that you use every time you need it. This way you are sure you use the same zone in all the `atZone()` calls. – Ole V.V. Jun 22 '17 at 09:36

3 Answers3

6

The TemporalAccessor interface can be used to do this. However, be aware that TemporalAccessor is an advanced interface that should not be used outside low-level utility code.

boolean api(TemporalAccessor temporal1, TemporalAccessor temporal2) {
  LocalDate date1 = LocalDate.from(temporal1);
  LocalDate date2 = LocalDate.from(temporal2);
  return ...;
}

This code will now accept LocalDate, LocalDateTime, OffsetDateTime and , ZonedDateTime.

As mentioned in the comments, it is vital to only call ZoneId.systemDefault() once within a piece of business logic, as the value can change.

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
3

Well... LocalDateTime is an immutable composition of LocalDate and LocalTime. It has methods toLocalDate() and toLocalTime() with which you can "decompose it". The opposite is also true - you can also pretty easily create a composition - see LocalDate.atTime(LocalTime).

It seems natural to me that if you consider time in your logic your extracted API-s should accept LocalDateTime. In the suspicious cases :) in which you want to feed for some reason this API with LocalDate-s you may explicitly use, for example, LocalDate.atStartOfDay. Something as this:

boolean api(LocalDate aDate, LocalDate anotherDate)
{
    return this.api(aDate.atStartOfDay(), anotherDate.atStartOfDay());
}

boolean api(LocalDateTime aDateTime, LocalDateTime anotherDateTime)
{
    return ...;
}

Which seems a bit ugly though but will still work pretty fine, won't it?

Lachezar Balev
  • 11,498
  • 9
  • 49
  • 72
0

You can make your method receives a java.time.temporal.Temporal as parameter (or a TemporalAccessor as suggested by @JodaStephen). This code works for both.

I must admit that I'd prefer to do it like @Lachezar Balev's answer is suggesting, but anyway, here's an alternative.

As a Temporal can be of any type (LocalDate, LocalDateTime, ZonedDateTime, etc), but you want just LocalDate and LocalDateTime, I'm doing the following:

  • try to create a LocalDateTime first: it requires more fields than a LocalDate (hour/minute/second), so if it can be created, it's the prefered type
  • if it can't be created, I catch the respective exception and try to create a LocalDate

Another detail is that I also simplified your comparison:

dateFieldToCheck.isAfter(minDate) || dateFieldToCheck.isEqual(minDate)

Is equivalent to:

! dateFieldToCheck.isBefore(minDate)

And a similar simplification was done when comparing with maxDate:

public boolean check(Temporal dateFieldToCheck, Temporal minDate, Temporal maxDate) {
    // try to get as LocalDateTime
    try {
        LocalDateTime dtCheck = LocalDateTime.from(dateFieldToCheck);
        LocalDateTime dtMin = LocalDateTime.from(minDate);
        LocalDateTime dtMax = LocalDateTime.from(maxDate);

        return (!dtCheck.isBefore(dtMin)) && (!dtCheck.isAfter(dtMax));
    } catch (DateTimeException e) {
        // exception: one of the Temporal objects above doesn't have all fields required by a LocalDateTime
        // trying a LocaDate instead
        LocalDate dCheck = LocalDate.from(dateFieldToCheck);
        LocalDate dMin = LocalDate.from(minDate);
        LocalDate dMax = LocalDate.from(maxDate);

        return (!dCheck.isBefore(dMin)) && (!dCheck.isAfter(dMax));
    }
}

Now you can call this method with both a LocalDate and a LocalDateTime:

check(LocalDate.of(2017, 6, 19), LocalDate.of(2017, 6, 18), LocalDate.of(2017, 6, 29)); // true
check(LocalDate.of(2017, 6, 17), LocalDate.of(2017, 6, 18), LocalDate.of(2017, 6, 29)); // false
check(LocalDateTime.of(2017, 6, 29, 9, 0), LocalDateTime.of(2017, 6, 29, 8, 0), LocalDateTime.of(2017, 6, 29, 11, 0)); // true
check(LocalDateTime.of(2017, 6, 29, 7, 0), LocalDateTime.of(2017, 6, 29, 8, 0), LocalDateTime.of(2017, 6, 29, 11, 0)); // false

You can extend the code to recognize other types, if you need (such as LocalTime).

You could also add another parameter indicating what type will be used, like this:

public boolean check(Temporal dateFieldToCheck, Temporal minDate, Temporal maxDate, int type) {
    switch (type) {
        case 1:
            // try to get as LocalDateTime
            LocalDateTime dtCheck = LocalDateTime.from(dateFieldToCheck);
            LocalDateTime dtMin = LocalDateTime.from(minDate);
            LocalDateTime dtMax = LocalDateTime.from(maxDate);

            return (!dtCheck.isBefore(dtMin)) && (!dtCheck.isAfter(dtMax));
        case 2:
            // trying a LocaDate instead
            LocalDate dCheck = LocalDate.from(dateFieldToCheck);
            LocalDate dMin = LocalDate.from(minDate);
            LocalDate dMax = LocalDate.from(maxDate);

            return (!dCheck.isBefore(dMin)) && (!dCheck.isAfter(dMax));
        // ... and so on
    }

    // invalid type, return false?
    return false;
}

So you could call it with a ZonedDateTime and force it to use a LocalDate (then you could adapt your code and call this method with the objects you've got from atZone(ZoneId.systemDefault())):

ZonedDateTime z1 = obj.getDate(fc.getDateField()).toInstant().atZone(ZoneId.systemDefault());
ZonedDateTime z2 = context.getStartDate().toInstant().atZone(ZoneId.systemDefault());
ZonedDateTime z3 = context.getEndDate().toInstant().atZone(ZoneId.systemDefault());
check(z1, z2, z3, 2);

This code also works with different types:

ZonedDateTime z1 = ...
LocalDate d = ...
LocalDateTime dt = ...
// converts all to LocalDate
System.out.println(check(z1, dt, d, 2));

PS: of course you could create an Enum instead of an int to define the type. Or use a Class<? extends Temporal> to directly indicate the type:

public boolean check(Temporal dateFieldToCheck, Temporal minDate, Temporal maxDate, Class<? extends Temporal> type) {
    if (type == LocalDate.class) {
        // code for LocalDate (use LocalDate.from() as above)
    }
    if (type == LocalDateTime.class) {
        // code for LocalDateTime (use LocalDateTime.from() as above)
    }
}

// convert all objects to LocalDate
check(z1, dt, d, LocalDate.class);