From b7ec70bc2978da30a70077eb313f81f4b6320449 Mon Sep 17 00:00:00 2001 From: Tore Langedal Endestad Date: Mon, 28 Mar 2022 18:46:22 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20St=C3=B8tte=20tidsserie=20som=20slutter?= =?UTF-8?q?=20p=C3=A5=20LocalDate.MAX=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Støtte tidslinjer som slutter på LocalDate.MAX --- README.MD | 2 +- .../fpsak/tidsserie/LocalDateTimeline.java | 73 ++++++++++--------- .../tidsserie/LocalDateTimelineTest.java | 58 ++++++++++----- 3 files changed, 77 insertions(+), 56 deletions(-) diff --git a/README.MD b/README.MD index 5e7c56f..f6d63ed 100644 --- a/README.MD +++ b/README.MD @@ -21,7 +21,7 @@ F.eks. vil det automatisk holde rede på knekkpunkter, kunne kombinere tidsserie no.nav.fpsak.tidsserie fpsak-tidsserie - 2.3-20200427084950-396F346 + 2.6.2 ``` diff --git a/src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java b/src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java index 5283093..eb932e7 100644 --- a/src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java +++ b/src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java @@ -39,8 +39,6 @@ *

* Derimot kan to tidsserier kombineres vha. en {@link LocalDateSegmentCombinator} funksjon. *

- * De fleste algoritmene for å kombinere to tidsserier er O(n^2) og ikke optimale for sammenstilling av 1000+ verdier. - *

* API'et er modellert etter metoder fra java.time og threeten-extra Interval. * * @param Java type for verdier i tidsserien. @@ -235,21 +233,22 @@ public LocalDateTimeline combine(final LocalDateTimeline other, fin Iterator> rhsIterator = other.segments.iterator(); LocalDateSegment lhs = lhsIterator.hasNext() ? lhsIterator.next() : null; LocalDateSegment rhs = rhsIterator.hasNext() ? rhsIterator.next() : null; - Iterator startdatoIterator = new KnekkpunktIterator<>(this.segments, other.segments); + KnekkpunktIterator 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 tilpassetLhsSegment = harLhs ? splittVedDelvisOverlapp(this.segmentSplitter, lhs, periode) : null; LocalDateSegment tilpassetRhsSegment = harRhs ? splittVedDelvisOverlapp(other.segmentSplitter, rhs, periode) : null; LocalDateSegment nyVerdi = combinator.combine(periode, tilpassetLhsSegment, tilpassetRhsSegment); @@ -257,7 +256,7 @@ public LocalDateTimeline combine(final LocalDateTimeline other, fin combinedSegmenter.add(nyVerdi); } } - fom = nesteFom; + fomEpochDay = nesteFomEpochDay; } return new LocalDateTimeline<>(combinedSegmenter); } @@ -784,15 +783,15 @@ private NavigableMap joinLocalDateIntervals(Navigabl * Finner alle knekkpunkter fra to tidslinjer, i sekvens. *

* 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 - *

- * 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 implements Iterator { + private static class KnekkpunktIterator { private final Iterator> lhsIterator; private final Iterator> rhsIterator; private LocalDateSegment lhsSegment; private LocalDateSegment rhsSegment; - private LocalDate next; + private Long next; public KnekkpunktIterator(NavigableSet> lhsIntervaller, NavigableSet> rhsIntervaller) { lhsIterator = lhsIntervaller.iterator(); @@ -800,53 +799,57 @@ public KnekkpunktIterator(NavigableSet> lhsIntervaller, Navi 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 LocalDate oppdaterKandidatForNeste(LocalDate forrige, LocalDate besteKandidat, LocalDateSegment 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 void oppdaterNeste(Long forrige, LocalDateSegment 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; } } diff --git a/src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java b/src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java index 972e7ca..207a89b 100644 --- a/src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java +++ b/src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java @@ -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 tidslinje = basicContinuousTimeline(); Assertions.assertThat(tidslinje.isContinuous()).isTrue(); } @Test - public void skal_opprette_ikke_kontinuerlig_tidslinje() throws Exception { + void skal_opprette_ikke_kontinuerlig_tidslinje() { LocalDateTimeline 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 continuousTimeline = basicContinuousTimeline(); Assertions .assertThat(continuousTimeline.intersection(continuousTimeline, StandardCombinators::coalesceLeftHandSide)) @@ -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 timeline = basicDiscontinuousTimeline(); @@ -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 timeline = basicDiscontinuousTimeline(); LocalDate today = LocalDate.now(); @@ -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 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 continuousTimeline = basicContinuousTimeline(); Assertions.assertThat(continuousTimeline.disjoint(continuousTimeline, StandardCombinators::coalesceLeftHandSide)) .isEqualTo(LocalDateTimeline.EMPTY_TIMELINE); @@ -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 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 timeline = basicContinuousTimeline(); CharSequence json = new JsonTimelineFormatter().formatJson(timeline); @@ -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); @@ -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); @@ -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); @@ -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> 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)); @@ -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> 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)); @@ -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 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 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 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 tidslinje = basicDiscontinuousTimeline(); var segments = tidslinje.toSegments(); var segLast = segments.last(); @@ -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 tidslinjeA = new LocalDateTimeline<>(LocalDate.MIN, LocalDate.MAX, "A"); + LocalDateTimeline tidslinjeB = new LocalDateTimeline<>(LocalDate.MIN, LocalDate.MAX, "B"); + LocalDateTimeline 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 tidslinjeA = new LocalDateTimeline<>(LocalDate.MAX, LocalDate.MAX, "A"); + LocalDateTimeline tidslinjeB = new LocalDateTimeline<>(LocalDate.MAX, LocalDate.MAX, "B"); + LocalDateTimeline 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> segmenter = new ArrayList<>();