Skip to content

Commit

Permalink
fix: utvider signatur for splitregular for å forhindre .evig. loekke (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
frode-carlsen authored Oct 15, 2020
1 parent 91f4a96 commit 3a65514
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 65 deletions.
132 changes: 69 additions & 63 deletions src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public LocalDateTimeline(Collection<LocalDateSegment<V>> datoSegmenter,
/**
* Constructor
*
* @param datoSegmenter - segmenter i tidsserien
* @param datoSegmenter - segmenter i tidsserien
* @param overlapCombinator - Optional combinator dersom noen segmenter overlapper. Må spesifiseres dersom det er sannsynlig kan skje.
*/
public LocalDateTimeline(Collection<LocalDateSegment<V>> datoSegmenter,
Expand All @@ -112,8 +112,8 @@ public LocalDateTimeline(Collection<LocalDateSegment<V>> datoSegmenter,
/**
* Constructor
*
* @param datoSegmenter - segmenter i tidsserien
* @param overlapCombinator - Optional combinator dersom noen segmenter overlapper. Må spesifiseres dersom det er sannsynlig kan skje.
* @param datoSegmenter - segmenter i tidsserien
* @param overlapCombinator - Optional combinator dersom noen segmenter overlapper. Må spesifiseres dersom det er sannsynlig kan skje.
* @param customSegmentSplitter - Optional splitter dersom enkelt segmenter må splittes. Default splittes interval med konstant verdi.
*/
public LocalDateTimeline(Collection<LocalDateSegment<V>> datoSegmenter,
Expand All @@ -136,8 +136,8 @@ public LocalDateTimeline(Collection<LocalDateSegment<V>> datoSegmenter,
* Hvis ikke det gjøers kan f.eks. sjekker som tar hensyn til tidligere intervaller måtte sjekke om flere av disse er
* lenket og det går utover ytelsen.
*
* @param test - Angitt predicate tar inn liste av tidligere aksepterte segmenter, samt nytt segment (som kan angi en
* tom verdi) hvis gaps inkluderes.
* @param test - Angitt predicate tar inn liste av tidligere aksepterte segmenter, samt nytt segment (som kan angi en
* tom verdi) hvis gaps inkluderes.
* @param includeGaps - hvorvidt gaps testes for seg (vil ha null som verdi).
*/

Expand Down Expand Up @@ -166,7 +166,7 @@ public void accept(LocalDateSegment<V> t) {
LocalDateInterval segInterval = seg.getLocalDateInterval();
if (!(prevInterval.abuts(segInterval) || prevInterval.equals(segInterval))) {
LocalDateSegment<V> emptySegment = LocalDateSegment.emptySegment(prevInterval.getTomDato().plusDays(1),
segInterval.getFomDato().minusDays(1));
segInterval.getFomDato().minusDays(1));
evaluator.accept(emptySegment);
}
}
Expand All @@ -176,7 +176,7 @@ public void accept(LocalDateSegment<V> t) {
return new LocalDateTimeline<>(evaluator.segmenter);

}

/**
* Kombinerer denne tidsserien med et enkelt segment (convenience function)
*
Expand Down Expand Up @@ -205,21 +205,21 @@ public <T, R> LocalDateTimeline<R> combine(final LocalDateTimeline<T> other, fin

// Join alle intervaller
final NavigableMap<LocalDateInterval, Integer> joinDatoInterval = joinLocalDateIntervals(getDatoIntervaller(),
other.getDatoIntervaller());
other.getDatoIntervaller());

// filtrer ut i henhold til combinationStyle
final List<LocalDateSegment<R>> combinedSegmenter = new ArrayList<>();
final LocalDateTimeline<V> myTidslinje = this;
joinDatoInterval.entrySet().stream()
.filter(e -> combinationStyle.accept(e.getValue()))
.forEachOrdered(e -> {
LocalDateInterval key = e.getKey();
LocalDateSegment<R> nyVerdi = combinator.combine(key, myTidslinje.getSegment(key),
other.getSegment(key));
if (nyVerdi != null) {
combinedSegmenter.add(nyVerdi);
}
});
.filter(e -> combinationStyle.accept(e.getValue()))
.forEachOrdered(e -> {
LocalDateInterval key = e.getKey();
LocalDateSegment<R> nyVerdi = combinator.combine(key, myTidslinje.getSegment(key),
other.getSegment(key));
if (nyVerdi != null) {
combinedSegmenter.add(nyVerdi);
}
});

return new LocalDateTimeline<>(combinedSegmenter);
}
Expand All @@ -231,20 +231,21 @@ public <T, R> LocalDateTimeline<R> combine(final LocalDateTimeline<T> other, fin
public LocalDateTimeline<V> compress() {
var factory = new CompressorFactory<V>(Objects::equals, (i, lhs, rhs) -> new LocalDateSegment<>(i, lhs.getValue()));
TimelineCompressor<V> compressor = segments.stream()
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);

return new LocalDateTimeline<>(compressor.segmenter);
}

/**
* Fikser opp tidslinjen ved å slå sammen sammenhengende intervall med "like" verider utover periode.
* @param e - likhetspredikat for å sammenligne to segment som vurderes slått sammen
* @param c - combinator for å slå sammen to tids-abut-segmenter som oppfyller e
*
* @param e - likhetspredikat for å sammenligne to segment som vurderes slått sammen
* @param c - combinator for å slå sammen to tids-abut-segmenter som oppfyller e
*/
public LocalDateTimeline<V> compress(BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
var factory = new CompressorFactory<>(e, c);
TimelineCompressor<V> compressor = segments.stream()
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);

return new LocalDateTimeline<>(compressor.segmenter);
}
Expand All @@ -258,29 +259,32 @@ public <T, R> LocalDateTimeline<R> crossJoin(LocalDateTimeline<T> other, LocalDa

/**
* Disjoint this timeline with given interval. Returns all parts of this timeline not overlapping with given
* interval. Can be used to cut out a given interval from this timeline.
* interval. Can be used to cut out a given interval from this timeline.
*
* @return new timeline without given interval.
*/
public LocalDateTimeline<V> disjoint(LocalDateInterval datoInterval) {
LocalDateTimeline<V> intervalTimeline = new LocalDateTimeline<>(Arrays.asList(new LocalDateSegment<V>(datoInterval, null)));
return this.disjoint(intervalTimeline, StandardCombinators::leftOnly);
}

public LocalDateTimeline<V> splitAtRegular(LocalDate startDate, Period period){
LocalDate endDate = getMaxLocalDate();


/**
* Splitter timeline i intervaller fra #startDate inntil #endDate med angitt periode.
* NB: vær forsiktig dersom det er åpen intervaller (tidenes ende, {@link LocalDate#MIN}, {@link LocalDate#MAX} el.
*/
public LocalDateTimeline<V> splitAtRegular(LocalDate startDate, LocalDate endDate, Period period) {
// nye segmenter
List<LocalDateSegment<V>> segmenter = new ArrayList<>();

LocalDate dt = startDate;
while(!dt.isAfter(endDate)) {
LocalDate nextDt = dt.plus(period);
while (!dt.isAfter(endDate)) {
LocalDate nextDt = dt.plus(period);
// trekk 1 fra nextDt siden vi har fom/tom (ikke fom /til)
var nesteSegmenter = intersection(new LocalDateInterval(dt, nextDt.minusDays(1))).toSegments();
segmenter.addAll(nesteSegmenter);
dt = nextDt;
}

return new LocalDateTimeline<>(segmenter);
}

Expand Down Expand Up @@ -429,8 +433,8 @@ public boolean isContinuous() {
public boolean isContinuous(LocalDateInterval matchInterval) {
LocalDateTimeline<V> intersection = this.intersection(matchInterval);
return !intersection.isEmpty() && intersection.isContinuous()
&& Objects.equals(intersection.getMinLocalDate(), matchInterval.getFomDato())
&& Objects.equals(intersection.getMaxLocalDate(), matchInterval.getTomDato());
&& Objects.equals(intersection.getMinLocalDate(), matchInterval.getFomDato())
&& Objects.equals(intersection.getMaxLocalDate(), matchInterval.getTomDato());
}

public boolean isEmpty() {
Expand Down Expand Up @@ -488,40 +492,41 @@ public int size() {
public NavigableSet<LocalDateSegment<V>> toSegments() {
return Collections.unmodifiableNavigableSet(segments);
}

@Override
public Iterator<LocalDateSegment<V>> iterator() {
return toSegments().iterator();
}

public Stream<LocalDateSegment<V>> stream() {
return segments.stream();
}

/** Find timeline of unique segments in collection of segments that may overlap. */
public static<V> LocalDateTimeline<List<V>> buildGroupOverlappingSegments(Collection<LocalDateSegment<V>> segmentsWithPossibleOverlaps){
public static <V> LocalDateTimeline<List<V>> buildGroupOverlappingSegments(Collection<LocalDateSegment<V>> segmentsWithPossibleOverlaps) {
@SuppressWarnings({ "cast" })
var uniqueSegments = segmentsWithPossibleOverlaps.stream().map(s -> new LocalDateSegment<>(s.getLocalDateInterval(), (List<V>)new ArrayList<V>())).collect(Collectors.toList());
var uniqueSegments = segmentsWithPossibleOverlaps.stream().map(s -> new LocalDateSegment<>(s.getLocalDateInterval(), (List<V>) new ArrayList<V>()))
.collect(Collectors.toList());
var uniqueIntervalTimeline = new LocalDateTimeline<>(uniqueSegments, (interval, lhs, rhs) -> new LocalDateSegment<>(interval, new ArrayList<V>()));

for(var per : uniqueIntervalTimeline.toSegments()) {
for(var seg: segmentsWithPossibleOverlaps) {
if(seg.getLocalDateInterval().overlaps(per.getLocalDateInterval())) {
for (var per : uniqueIntervalTimeline.toSegments()) {
for (var seg : segmentsWithPossibleOverlaps) {
if (seg.getLocalDateInterval().overlaps(per.getLocalDateInterval())) {
per.getValue().add(seg.getValue());
}
}
}
return uniqueIntervalTimeline;
}

@Override
public String toString() {
return getClass().getSimpleName() + "<" + //$NON-NLS-1$
(isEmpty() ? "0" //$NON-NLS-1$
: getMinLocalDate() + ", " + getMaxLocalDate()) //$NON-NLS-1$
+ " [" + size() + "]" //$NON-NLS-1$ // $NON-NLS-2$
+ "> = [" + getDatoIntervaller().stream().map(d -> String.valueOf(d)).collect(Collectors.joining(",")) + "]" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
;
(isEmpty() ? "0" //$NON-NLS-1$
: getMinLocalDate() + ", " + getMaxLocalDate()) //$NON-NLS-1$
+ " [" + size() + "]" //$NON-NLS-1$ // $NON-NLS-2$
+ "> = [" + getDatoIntervaller().stream().map(d -> String.valueOf(d)).collect(Collectors.joining(",")) + "]" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
;
}

/**
Expand Down Expand Up @@ -556,7 +561,7 @@ private boolean add(LocalDateSegment<V> datoSegment, LocalDateSegmentCombinator<
*/
public boolean isTimelineOutsideInterval(LocalDateInterval datoInterval) {
return !(datoInterval.getFomDato().isBefore(this.getMaxLocalDate().plusDays(1))
&& datoInterval.getTomDato().isAfter(this.getMinLocalDate().minusDays(1)));
&& datoInterval.getTomDato().isAfter(this.getMinLocalDate().minusDays(1)));
}

private void addWhenOverlap(LocalDateSegment<V> datoSegment, LocalDateSegmentCombinator<V, V, V> overlapCombinator,
Expand All @@ -569,15 +574,15 @@ private void addWhenOverlap(LocalDateSegment<V> datoSegment, LocalDateSegmentCom

// håndter intervaller som eksisterer fra før, men ikke overlapper nytt
segInterval.except(datoInterval)
.forEach(di -> newSegments.add(new LocalDateSegment<>(di, segEntry.getValue())));
.forEach(di -> newSegments.add(new LocalDateSegment<>(di, segEntry.getValue())));

// håndter gap i eksisterende serie der ny har verdi
håndterGapIEksisterende(datoSegment, datoInterval, newSegments, segEntry);

// håndter overlapp mellom eksisterende serie og ny verdi
Optional<LocalDateInterval> segOverlap = segInterval.overlap(datoInterval);
LocalDateInterval segOverlappInterval = segOverlap.orElseThrow(() -> new IllegalArgumentException(
String.format("Utvikler-feil: intervall overlapper ikke : %s - %s", segInterval, datoInterval)));
String.format("Utvikler-feil: intervall overlapper ikke : %s - %s", segInterval, datoInterval)));

newSegments.add(overlapCombinator.combine(segOverlappInterval, segEntry, datoSegment));
} else {
Expand All @@ -588,7 +593,8 @@ private void addWhenOverlap(LocalDateSegment<V> datoSegment, LocalDateSegmentCom
segments.addAll(newSegments);
}

private void håndterGapIEksisterende(LocalDateSegment<V> datoSegment, LocalDateInterval datoInterval, List<LocalDateSegment<V>> newSegments, LocalDateSegment<V> segEntry) {
private void håndterGapIEksisterende(LocalDateSegment<V> datoSegment, LocalDateInterval datoInterval, List<LocalDateSegment<V>> newSegments,
LocalDateSegment<V> segEntry) {
LocalDateSegment<V> nesteSegment = segments.higher(segEntry);
LocalDateSegment<V> forrigeSegment = segments.lower(segEntry);

Expand Down Expand Up @@ -643,15 +649,15 @@ private NavigableSet<LocalDateInterval> combineAllUniqueIntervals(NavigableSet<L

List<LocalDateInterval> overlapped = new ArrayList<>();
List<LocalDateInterval> split = result.stream()
.filter(i -> {
boolean ret = i.overlaps(interval);
if (ret) {
overlapped.add(i);
}
return ret;
})
.flatMap(i -> i.splitAll(interval).stream())
.collect(Collectors.toList());
.filter(i -> {
boolean ret = i.overlaps(interval);
if (ret) {
overlapped.add(i);
}
return ret;
})
.flatMap(i -> i.splitAll(interval).stream())
.collect(Collectors.toList());

if (split.isEmpty()) {
result.add(interval);
Expand Down Expand Up @@ -707,7 +713,7 @@ private NavigableMap<LocalDateInterval, Integer> joinLocalDateIntervals(Navigabl

private <T> boolean skipNonMatchingInnerJoin(final LocalDateTimeline<T> other, final JoinStyle combinationStyle) {
return JoinStyle.INNER_JOIN == combinationStyle
&& ((this.isEmpty() || other.isEmpty()) || !new LocalDateInterval(this.getMinLocalDate(), this.getMaxLocalDate())
&& ((this.isEmpty() || other.isEmpty()) || !new LocalDateInterval(this.getMinLocalDate(), this.getMaxLocalDate())
.overlaps(new LocalDateInterval(other.getMinLocalDate(), other.getMaxLocalDate())));
}

Expand Down Expand Up @@ -796,9 +802,9 @@ public interface Reducer<V, R> {
}

/** Interface for å custom funksjon for å splitte segmenter. */
public interface SegmentSplitter<V> extends BiFunction<LocalDateInterval, LocalDateSegment<V>, LocalDateSegment<V>>{
public interface SegmentSplitter<V> extends BiFunction<LocalDateInterval, LocalDateSegment<V>, LocalDateSegment<V>> {
}

private static final class EqualValueSegmentSplitter<V> implements SegmentSplitter<V>, Serializable {
@Override
public LocalDateSegment<V> apply(LocalDateInterval di, LocalDateSegment<V> seg) {
Expand All @@ -822,7 +828,7 @@ static class TimelineCompressor<V> implements Consumer<LocalDateSegment<V>> {

TimelineCompressor(BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
this.equals = e;
this.combinator =c;
this.combinator = c;
}

@Override
Expand All @@ -832,7 +838,7 @@ public void accept(LocalDateSegment<V> t) {
} else {
LocalDateSegment<V> last = segmenter.last();
if (last.getLocalDateInterval().abuts(t.getLocalDateInterval())
&& equals.test(last.getValue(), t.getValue())) {
&& equals.test(last.getValue(), t.getValue())) {
// bytt ut og ekspander intervall for siste
segmenter.remove(last);
LocalDateInterval expandedInterval = last.getLocalDateInterval().expand(t.getLocalDateInterval());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,9 @@ public void eksempel_splitt_av_tidsserie_ved_period_year() throws Exception {
toSegment("2019-12-01", "2020-12-31", "A"),
toSegment("2017-01-01", "2017-12-31", "B")));

var mappedTimeline = timeline.splitAtRegular(LocalDate.parse("2016-01-01"), Period.ofYears(1));
LocalDate startDate = LocalDate.parse("2016-01-01");
LocalDate endDate = timeline.getMaxLocalDate();
var mappedTimeline = timeline.splitAtRegular(startDate, endDate, Period.ofYears(1));

var expectedTimeline = new LocalDateTimeline<>(
List.of(
Expand All @@ -421,7 +423,9 @@ public void eksempel_splitt_av_tidsserie_ved_period_day_3() throws Exception {
List.of(
toSegment("2019-12-01", "2020-01-02", "A")));

var mappedTimeline = timeline.splitAtRegular(LocalDate.parse("2019-12-01"), Period.ofDays(3));
LocalDate startDate = LocalDate.parse("2019-12-01");
LocalDate endDate = timeline.getMaxLocalDate().plusDays(1); // ta med litt ekstra
var mappedTimeline = timeline.splitAtRegular(startDate, endDate, Period.ofDays(3));

var expectedTimeline = new LocalDateTimeline<>(
List.of(
Expand All @@ -439,6 +443,29 @@ public void eksempel_splitt_av_tidsserie_ved_period_day_3() throws Exception {

assertThat(mappedTimeline).isEqualTo(expectedTimeline);
}

@Test
public void eksempel_splitt_av_tidsserie_ved_period_week_1() throws Exception {

var timeline = new LocalDateTimeline<>(
List.of(
toSegment("2019-12-01", "2020-01-02", "A")));

LocalDate startDate = LocalDate.parse("2019-12-01");
LocalDate endDate = timeline.getMaxLocalDate();
var mappedTimeline = timeline.splitAtRegular(startDate, endDate, Period.ofWeeks(1));

var expectedTimeline = new LocalDateTimeline<>(
List.of(
toSegment("2019-12-01", "2019-12-07", "A"),
toSegment("2019-12-08", "2019-12-14", "A"),
toSegment("2019-12-15", "2019-12-21", "A"),
toSegment("2019-12-22", "2019-12-28", "A"),
toSegment("2019-12-29", "2020-01-02", "A")));

assertThat(mappedTimeline).isEqualTo(expectedTimeline);
}


private static <V> LocalDateSegment<V> toSegment(String dt1, String dt2, V val) {
return new LocalDateSegment<>(LocalDate.parse(dt1), LocalDate.parse(dt2), val);
Expand Down

0 comments on commit 3a65514

Please sign in to comment.