4

I have a custom range (~ Collection) which has 2 Temporal bounds (from and to) and can enumerate all values between those 2 bounds in time by incrementing with a given TemporalUnit incrementUnitType.

private final Temporal_ from;
private final Temporal_ to;
private final TemporalUnit incrementUnitType; // For example Month, Day,  Minute, ...

In it, I need to implement a contains method, to check if iterating through that range would contain a specific value. For example if it contains 8 March. Here's how I 'd like to write that method:

public boolean contains(Temporal_ value) {
    ...
    if (from.until(value, incrementUnitType) < 0
            || value.until(to, incrementUnitType) <= 0) {
        return false; // Out of bounds
    }
    // This doesn't work because 1-MAR + 1 month doesn't include 8-MAR
    return true;
}

Here's a few iterations:

  • from - to (incrementUnitType)
  • 1-MAR - 10-APR (1 day): 1-MAR, 2-MAR, 3-MAR, 4-MAR, ..., 8-APR, 9-APR
  • 1-MAR - 10-APR (1 week): 1-MAR, 8-MAR, 15-MAR, 22-MAR, 29-MAR, 5-APR
  • 1-MAR - 10-APR (1 month): 1-MAR, 1-APR

The code above would incorrect return true for 8-MAR in the last case. Here's how I need to write that code to work, by doing a for loop and check every possible value:

public boolean contains(Temporal_ value) {
    ...
    if (from.until(value, incrementUnitType) < 0
            || value.until(to, incrementUnitType) <= 0) {
        return false; // Out of bounds
    }
    // This works but it kills scalability
    for (long i = 0; i < getSize(); i++) {
        Temporal_ temporal = get(i);
        if (value.equals(temporal)) {
            return true;
        }
    }
    return false;
}

That's a scalability issue. Is there any way I can avoid that?

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
Geoffrey De Smet
  • 26,223
  • 11
  • 73
  • 120

2 Answers2

3

You can use the method Temporal.until to get your result. As it returns "the number of complete units between the two temporals" (doc), you can add this number to from and see if it equals the value, but that does not work right away.

long wholeAmount = from.until(value, incrementUnitType)
return value.equals(from.plus(wholeAmount, incrementUnitType));

As you noticed, the problem is that some temporal units are not always the same duration (a month may be 28, 29, 30 or 31 day long for instance). until and plus may not be consistent ( i.e. temporal.until(temporal.plus(1,ChronoUnit.MONTHS),ChronoUnit.MONTHS) may be 0 while we added one month) .

Quoting the doc :

In some cases, changing a field is not fully defined. For example, if the target object is a date representing the 31st January, then adding one month would be unclear. In cases like this, the field is responsible for resolving the result. Typically it will choose the previous valid date, which would be the last valid day of February in this example.

This means the we should also check for wholeAmount + 1, just in case the whole amount was rounded.

// ... out of bound check
// ...
long wholeAmount = from.until(value, incrementUnitType)
return value.equals(from.plus(wholeAmount, incrementUnitType)) || value.equals(from.plus(wholeAmount + 1, incrementUnitType));

Dealing with Temporal seems tricky so I cannot be sure that it will work with every unit and every temporal (be sure to check that incrementUnitType is compatible with your temporal). There are probably several edge cases which should be tested.

archz
  • 1,073
  • 13
  • 19
  • Works like a charm (without the + 1). – Geoffrey De Smet Apr 12 '16 at 15:27
  • Indeed, It seems I misinterpreted the doc. going to edit that. – archz Apr 12 '16 at 15:34
  • **This doesn't work**: `2017-01-31` plus 1 month is `2017-02-28`, but `2017-01-31` until `2017-02-28` is 0 months. – Geoffrey De Smet Apr 13 '16 at 08:31
  • It looks like `until()` and `plus()` aren't each others inverse. [I've posted a separate question about that](http://stackoverflow.com/questions/36593101/java-date-and-time-how-do-i-make-plus-and-until-to-be-each-others-inverse) – Geoffrey De Smet Apr 13 '16 at 08:47
  • 1
    Good catch. This only work if the unit duration does not change depending on context : / . This inconsistency between `until` and `plus` is a bit odd though. I guess you may use this solution for units smaller than month, but there may be some other gotchas due to leap seconds even for these. Using JodaStephen solution may be the way to go. Another possibility would be to keep your implementation but use a binary search algorithm. This would keep the complexity logarithmic and ensure you that the result is consistent with your range object. – archz Apr 13 '16 at 08:58
  • 1
    In fact this can be solved just by also checking `wholeAmount + 1` (works for february at least). However note that `temporal.plus(1,unit).plus(1,unit)` can be different than `temporal.plus(2,unit)`. Depending on how you iterate in your range, this may be problematic. – archz Apr 13 '16 at 20:18
  • Very interesting hack :) @JodaStephen in your date-and-time expert opinion, could this + 1 approach work for all temporals (DST, leap seconds, ...), at least in theory? – Geoffrey De Smet Apr 14 '16 at 06:56
2

The most efficient solution would be separate logic for each increment unit.

  • With DAYS you can just check if the date is between the start and end.
  • With WEEKS you can call toEpochDays() on both, subtract them and check if it divides by 7
  • With MONTHS you can check if the day-of-month is the same

I would caution against using Temporal as the input type if you can avoid it, as it makes processing harder. One option if you must use it it to call LocalDate.from(temporal) on the input to allow the logic to work internally on LocalDate.

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
  • I am writing a general usage [TemporalValueRange](https://github.com/droolsjbpm/optaplanner/blob/master/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/valuerange/buildin/temporal/TemporalValueRange.java#L15) so all Temporal implementations are supported in OptaPlanner (not just LocalDate, LocalDateTime and LocalTime). – Geoffrey De Smet Apr 12 '16 at 11:47
  • Interesting answer. I'll go with the easy implementation for now (other answer), but do you think that the performance / scalability gain of writing it this way would be significant? As I want to support local/zoned, years/.../seconds, ... it would be a lot of code. – Geoffrey De Smet Apr 13 '16 at 07:18