Skip to content

Commit

Permalink
Fix: Støtte tidsserie som slutter på LocalDate.MAX (#85)
Browse files Browse the repository at this point in the history
* Støtte tidslinjer som slutter på LocalDate.MAX
  • Loading branch information
tendestad authored Mar 28, 2022
1 parent 74edf72 commit b7ec70b
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ F.eks. vil det automatisk holde rede på knekkpunkter, kunne kombinere tidsserie
<dependency>
<groupId>no.nav.fpsak.tidsserie</groupId>
<artifactId>fpsak-tidsserie</artifactId>
<version>2.3-20200427084950-396F346</version>
<version>2.6.2</version>
</dependency>
```

Expand Down
73 changes: 38 additions & 35 deletions src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
* <p>
* Derimot kan to tidsserier kombineres vha. en {@link LocalDateSegmentCombinator} funksjon.
* <p>
* De fleste algoritmene for å kombinere to tidsserier er O(n^2) og ikke optimale for sammenstilling av 1000+ verdier.
* <p>
* API'et er modellert etter metoder fra java.time og threeten-extra Interval.
*
* @param <V> Java type for verdier i tidsserien.
Expand Down Expand Up @@ -235,29 +233,30 @@ public <T, R> LocalDateTimeline<R> combine(final LocalDateTimeline<T> other, fin
Iterator<LocalDateSegment<T>> rhsIterator = other.segments.iterator();
LocalDateSegment<V> lhs = lhsIterator.hasNext() ? lhsIterator.next() : null;
LocalDateSegment<T> rhs = rhsIterator.hasNext() ? rhsIterator.next() : null;
Iterator<LocalDate> startdatoIterator = new KnekkpunktIterator<>(this.segments, other.segments);
KnekkpunktIterator<V, T> startdatoIterator = new KnekkpunktIterator<>(this.segments, other.segments);
if (!startdatoIterator.hasNext()) {
return empty(); //begge input-tidslinjer var tomme
}

LocalDate fom = startdatoIterator.next();
long fomEpochDay = startdatoIterator.nextEpochDay();
while (startdatoIterator.hasNext()) {
LocalDate fom = LocalDate.ofEpochDay(fomEpochDay);
lhs = spolTil(lhs, lhsIterator, fom);
rhs = spolTil(rhs, rhsIterator, fom);

boolean harLhs = lhs != null && lhs.getLocalDateInterval().contains(fom);
boolean harRhs = rhs != null && rhs.getLocalDateInterval().contains(fom);
LocalDate nesteFom = startdatoIterator.next();
long nesteFomEpochDay = startdatoIterator.nextEpochDay();
if (combinationStyle.accept(harLhs, harRhs)) {
LocalDateInterval periode = new LocalDateInterval(fom, nesteFom.minusDays(1));
LocalDateInterval periode = new LocalDateInterval(fom, LocalDate.ofEpochDay(nesteFomEpochDay - 1));
LocalDateSegment<V> tilpassetLhsSegment = harLhs ? splittVedDelvisOverlapp(this.segmentSplitter, lhs, periode) : null;
LocalDateSegment<T> tilpassetRhsSegment = harRhs ? splittVedDelvisOverlapp(other.segmentSplitter, rhs, periode) : null;
LocalDateSegment<R> nyVerdi = combinator.combine(periode, tilpassetLhsSegment, tilpassetRhsSegment);
if (nyVerdi != null) {
combinedSegmenter.add(nyVerdi);
}
}
fom = nesteFom;
fomEpochDay = nesteFomEpochDay;
}
return new LocalDateTimeline<>(combinedSegmenter);
}
Expand Down Expand Up @@ -784,69 +783,73 @@ private NavigableMap<LocalDateInterval, Integer> joinLocalDateIntervals(Navigabl
* Finner alle knekkpunkter fra to tidslinjer, i sekvens.
* <p>
* Knekkpunkter er 'start av et intervall' og 'dagen etter slutt av et intervall'. Sistnevnte fordi det da kan være starten på et nytt intervall
* <p>
* Tidliger implementert ved å dytte alle knekkpunkter i et TreeSet og iterere, men dette er raskere O(n) vs O(n ln n), og bruker mindre minne
*
* Hvis slutt av intervall er LocalDate.MAX, er dagen etter ikke representerbar i LocalDate, derfor bruker denne klassen epochDay istedet
*/
private static class KnekkpunktIterator<V, T> implements Iterator<LocalDate> {
private static class KnekkpunktIterator<V, T> {
private final Iterator<LocalDateSegment<V>> lhsIterator;
private final Iterator<LocalDateSegment<T>> rhsIterator;
private LocalDateSegment<V> lhsSegment;
private LocalDateSegment<T> rhsSegment;
private LocalDate next;
private Long next;

public KnekkpunktIterator(NavigableSet<LocalDateSegment<V>> lhsIntervaller, NavigableSet<LocalDateSegment<T>> rhsIntervaller) {
lhsIterator = lhsIntervaller.iterator();
rhsIterator = rhsIntervaller.iterator();
lhsSegment = lhsIterator.hasNext() ? lhsIterator.next() : null;
rhsSegment = rhsIterator.hasNext() ? rhsIterator.next() : null;

next = lhsSegment != null ? lhsSegment.getFom() : null;
if (rhsSegment != null && (next == null || rhsSegment.getFom().isBefore(next))) {
next = rhsSegment.getFom();
next = lhsSegment != null ? lhsSegment.getFom().toEpochDay() : null;
if (rhsSegment != null && (next == null || rhsSegment.getFom().toEpochDay() < next)) {
next = rhsSegment.getFom().toEpochDay();
}
}

@Override
public LocalDate next() {
public long nextEpochDay() {
if (next == null) {
throw new NoSuchElementException("Ikke flere verdier igjen");
}
LocalDate denne = next;
long denne = next;
oppdaterNeste();
return denne;
}

@Override
public boolean hasNext() {
return next != null;
}

private void oppdaterNeste() {
while (lhsSegment != null && !lhsSegment.getTom().plusDays(1).isAfter(next)) {
spolUnderliggendeIteratorer();

Long forrige = next;
next = null;
//neste knekkpunkt kan komme fra hvilken som helst av de to tidsseriene, må sjekke begge
oppdaterNeste(forrige, lhsSegment);
oppdaterNeste(forrige, rhsSegment);
}

private void spolUnderliggendeIteratorer() {
while (lhsSegment != null && lhsSegment.getTom().toEpochDay() < next) {
lhsSegment = lhsIterator.hasNext() ? lhsIterator.next() : null;
}
while (rhsSegment != null && !rhsSegment.getTom().plusDays(1).isAfter(next)) {
while (rhsSegment != null && rhsSegment.getTom().toEpochDay() < next) {
rhsSegment = rhsIterator.hasNext() ? rhsIterator.next() : null;
}

LocalDate forrige = next;
//neste knekkpunkt kan komme fra hvilken som helst av de to tidsseriene, må sjekke begge
next = oppdaterKandidatForNeste(forrige, null, lhsSegment);
next = oppdaterKandidatForNeste(forrige, next, rhsSegment);
}

private static <X> LocalDate oppdaterKandidatForNeste(LocalDate forrige, LocalDate besteKandidat, LocalDateSegment<X> segment) {
if (segment != null) {
LocalDate fomKandidat = segment.getFom();
if (fomKandidat.isAfter(forrige) && (besteKandidat == null || fomKandidat.isBefore(besteKandidat))) {
return fomKandidat;
}
LocalDate tomKandidat = segment.getTom().plusDays(1);
if (tomKandidat.isAfter(forrige) && (besteKandidat == null || tomKandidat.isBefore(besteKandidat))) {
return tomKandidat;
private <X> void oppdaterNeste(Long forrige, LocalDateSegment<X> segment) {
if (segment == null) {
return; //underliggende iterator er tømt
}
long fomKandidat = segment.getFom().toEpochDay();
if (fomKandidat > forrige && (next == null || fomKandidat < next)) {
next = fomKandidat;
} else {
long tomKandidat = segment.getTom().toEpochDay() + 1;
if (tomKandidat > forrige && (next == null || tomKandidat < next)) {
next = tomKandidat;
}
}
return besteKandidat;
}
}

Expand Down
58 changes: 38 additions & 20 deletions src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@

import no.nav.fpsak.tidsserie.json.JsonTimelineFormatter;

public class LocalDateTimelineTest {
class LocalDateTimelineTest {

LocalDate today = LocalDate.now();

@Test
public void skal_opprette_kontinuerlig_tidslinje() throws Exception {
void skal_opprette_kontinuerlig_tidslinje() {
LocalDateTimeline<String> tidslinje = basicContinuousTimeline();
Assertions.assertThat(tidslinje.isContinuous()).isTrue();
}

@Test
public void skal_opprette_ikke_kontinuerlig_tidslinje() throws Exception {
void skal_opprette_ikke_kontinuerlig_tidslinje() {
LocalDateTimeline<String> tidslinje = basicDiscontinuousTimeline();
Assertions.assertThat(tidslinje.isContinuous()).isFalse();
}

@Test
public void skal_ha_equal_tidslinje_når_intersecter_seg_selv() throws Exception {
void skal_ha_equal_tidslinje_når_intersecter_seg_selv() {
LocalDateTimeline<String> continuousTimeline = basicContinuousTimeline();
Assertions
.assertThat(continuousTimeline.intersection(continuousTimeline, StandardCombinators::coalesceLeftHandSide))
Expand All @@ -46,7 +46,7 @@ public void skal_opprette_ikke_kontinuerlig_tidslinje() throws Exception {
}

@Test
public void skal_intersecte_annen_tidslinje() throws Exception {
void skal_intersecte_annen_tidslinje() {

LocalDateTimeline<String> timeline = basicDiscontinuousTimeline();

Expand All @@ -66,7 +66,7 @@ public void skal_intersecte_annen_tidslinje() throws Exception {
}

@Test
public void skal_cross_joine_annen_tidslinje() throws Exception {
void skal_cross_joine_annen_tidslinje() {
LocalDateTimeline<String> timeline = basicDiscontinuousTimeline();

LocalDate today = LocalDate.now();
Expand All @@ -89,14 +89,14 @@ public void skal_cross_joine_annen_tidslinje() throws Exception {

@SuppressWarnings("unchecked")
@Test
public void skal_cross_joine_tom_tidsserie() throws Exception {
void skal_cross_joine_tom_tidsserie() {
LocalDateTimeline<String> timeline = basicDiscontinuousTimeline();
assertThat(timeline.crossJoin(LocalDateTimeline.EMPTY_TIMELINE)).isEqualTo(timeline);
assertThat(LocalDateTimeline.EMPTY_TIMELINE.crossJoin(timeline)).isEqualTo(timeline);
}

@Test
public void skal_ha_empty_tidslinje_når_disjointer_seg_selv() throws Exception {
void skal_ha_empty_tidslinje_når_disjointer_seg_selv() {
LocalDateTimeline<String> continuousTimeline = basicContinuousTimeline();
Assertions.assertThat(continuousTimeline.disjoint(continuousTimeline, StandardCombinators::coalesceLeftHandSide))
.isEqualTo(LocalDateTimeline.EMPTY_TIMELINE);
Expand All @@ -109,14 +109,14 @@ public void skal_cross_joine_tom_tidsserie() throws Exception {
}

@Test
public void skal_formattere_timeline_som_json_output() throws Exception {
void skal_formattere_timeline_som_json_output() {
LocalDateTimeline<String> timeline = basicContinuousTimeline();
CharSequence json = new JsonTimelineFormatter().formatJson(timeline);
assertThat(json).isNotNull().contains(LocalDate.now().toString());
}

@Test
public void skal_formattere_timeline_som_json_output_uten_verdier() throws Exception {
void skal_formattere_timeline_som_json_output_uten_verdier() {
LocalDateTimeline<String> timeline = basicContinuousTimeline();

CharSequence json = new JsonTimelineFormatter().formatJson(timeline);
Expand All @@ -127,7 +127,7 @@ public void skal_formattere_timeline_som_json_output_uten_verdier() throws Excep
}

@Test
public void skal_compress_en_tidsserie_med_sammenhengende_intervaller_med_samme_verdi() throws Exception {
void skal_compress_en_tidsserie_med_sammenhengende_intervaller_med_samme_verdi() {

LocalDate d1 = LocalDate.now();
LocalDate d2 = d1.plusDays(2);
Expand All @@ -152,7 +152,7 @@ public void skal_compress_en_tidsserie_med_sammenhengende_intervaller_med_samme_
}

@Test
public void skal_ikke_compresse_en_tidsserie_uten_sammenhengende_intervaller_med_samme_verdi() throws Exception {
void skal_ikke_compresse_en_tidsserie_uten_sammenhengende_intervaller_med_samme_verdi() {

LocalDate d1 = LocalDate.now();
LocalDate d2 = d1.plusDays(2);
Expand All @@ -175,7 +175,7 @@ public void skal_ikke_compresse_en_tidsserie_uten_sammenhengende_intervaller_med
}

@Test
public void skal_gruppere_per_segment_periode() throws Exception {
void skal_gruppere_per_segment_periode() {
LocalDate d1 = LocalDate.now();
LocalDate d2 = d1.plusDays(2);
LocalDate d3 = d2.plusDays(1);
Expand Down Expand Up @@ -203,7 +203,7 @@ public void skal_gruppere_per_segment_periode() throws Exception {
}

@Test
public void skal_håndtere_overlapp_når_flere_perioder_overlapper_med_hverandre() {
void skal_håndtere_overlapp_når_flere_perioder_overlapper_med_hverandre() {
Set<LocalDateSegment<Boolean>> segementer = new HashSet<>();
LocalDateInterval førstePeriode = LocalDateInterval.withPeriodAfterDate(LocalDate.of(2015, 1, 1), Period.of(2, 0, 0));
LocalDateInterval andrePeriode = LocalDateInterval.withPeriodAfterDate(LocalDate.of(2015, 1, 1), Period.of(2, 9, 29));
Expand All @@ -220,7 +220,7 @@ public void skal_gruppere_per_segment_periode() throws Exception {
}

@Test
public void skal_håndtere_overlapp_når_flere_perioder_overlapper_med_hverandre_2() {
void skal_håndtere_overlapp_når_flere_perioder_overlapper_med_hverandre_2() {
Set<LocalDateSegment<Boolean>> segementer = new HashSet<>();
LocalDateInterval førstePeriode = LocalDateInterval.withPeriodAfterDate(LocalDate.of(2015, 2, 1), Period.of(0, 1, 1));
LocalDateInterval andrePeriode = LocalDateInterval.withPeriodAfterDate(LocalDate.of(2015, 4, 1), Period.of(0, 1, 1));
Expand All @@ -241,27 +241,27 @@ public void skal_gruppere_per_segment_periode() throws Exception {
}

@Test
public void skal_disjoint_samme_tidsserie() throws Exception {
void skal_disjoint_samme_tidsserie() {
LocalDateTimeline<String> tidslinje = basicDiscontinuousTimeline();
Assertions.assertThat(tidslinje.disjoint(tidslinje)).isEmpty();
}

@SuppressWarnings("unchecked")
@Test
public void skal_disjoint_tom_tidsserie() throws Exception {
void skal_disjoint_tom_tidsserie() {
LocalDateTimeline<String> tidslinje = basicDiscontinuousTimeline();
Assertions.assertThat(tidslinje.disjoint(LocalDateTimeline.EMPTY_TIMELINE)).isEqualTo(tidslinje);
}

@SuppressWarnings("unchecked")
@Test
public void skal_disjoint_med_tom_tidsserie() throws Exception {
void skal_disjoint_med_tom_tidsserie() {
LocalDateTimeline<String> tidslinje = basicDiscontinuousTimeline();
Assertions.assertThat(LocalDateTimeline.EMPTY_TIMELINE.disjoint(tidslinje)).isEqualTo(LocalDateTimeline.EMPTY_TIMELINE);
}

@Test
public void skal_disjoint_med_annen_tidsserie() throws Exception {
void skal_disjoint_med_annen_tidsserie() {
LocalDateTimeline<String> tidslinje = basicDiscontinuousTimeline();
var segments = tidslinje.toSegments();
var segLast = segments.last();
Expand All @@ -275,9 +275,27 @@ public void skal_disjoint_med_annen_tidsserie() throws Exception {
Assertions.assertThat(tidslinje.disjoint(annenTidsserie)).isEqualTo(resultatTidsserie);
}

@Test
void skal_håndtere_lengst_mulige_periode() {
LocalDateTimeline<String> tidslinjeA = new LocalDateTimeline<>(LocalDate.MIN, LocalDate.MAX, "A");
LocalDateTimeline<String> tidslinjeB = new LocalDateTimeline<>(LocalDate.MIN, LocalDate.MAX, "B");
LocalDateTimeline<String> tidslinjeAB = tidslinjeA.combine(tidslinjeB, StandardCombinators::concat, LocalDateTimeline.JoinStyle.INNER_JOIN);

assertThat(tidslinjeAB).isEqualTo(new LocalDateTimeline<>(LocalDate.MIN, LocalDate.MAX, "AB"));
}

@Test
void skal_håndtere_sendeste_mulige_dato() {
LocalDateTimeline<String> tidslinjeA = new LocalDateTimeline<>(LocalDate.MAX, LocalDate.MAX, "A");
LocalDateTimeline<String> tidslinjeB = new LocalDateTimeline<>(LocalDate.MAX, LocalDate.MAX, "B");
LocalDateTimeline<String> tidslinjeAB = tidslinjeA.combine(tidslinjeB, StandardCombinators::concat, LocalDateTimeline.JoinStyle.INNER_JOIN);

assertThat(tidslinjeAB).isEqualTo(new LocalDateTimeline<>(LocalDate.MAX, LocalDate.MAX, "AB"));
}

@Disabled("Micro performance test - kun for spesielt interesserte! Kan brukes til å avsjekke forbedringer i join algoritme")
@Test
public void kjapp_ytelse_test() throws Exception {
void kjapp_ytelse_test() {

List<LocalDateSegment<String>> segmenter = new ArrayList<>();

Expand Down

0 comments on commit b7ec70b

Please sign in to comment.