Skip to content

Commit

Permalink
Støtte for sammenhengende arbeidsdager (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
jolarsen authored Jan 24, 2023
1 parent 5fe9c72 commit 7f4d80e
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 12 deletions.
59 changes: 56 additions & 3 deletions src/main/java/no/nav/fpsak/tidsserie/LocalDateInterval.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package no.nav.fpsak.tidsserie;

import static java.time.temporal.TemporalAdjusters.next;

import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
import java.time.chrono.ChronoLocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjuster;
import java.util.Collections;
import java.util.Comparator;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiPredicate;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
Expand Down Expand Up @@ -89,13 +95,19 @@ public static LocalDateInterval withPeriodBeforeDate(Period period, LocalDate en
}

/**
* Hvorvidt to intervaller ligger rett ved siden av hverandre (at this.tomDato
* == other.fomDato - 1 eller vice versa).
* Hvorvidt to intervaller ligger rett ved siden av hverandre (at this.tomDato == other.fomDato - 1 eller vice versa).
*/
public boolean abuts(LocalDateInterval other) {
return getTomDato().equals(other.getFomDato().minusDays(1)) || other.getTomDato().equals(getFomDato().minusDays(1));
}

/**
* Hvorvidt to intervaller ligger rett ved siden av hverandre (at this.tomDato == other.fomDato - 1 eller vice versa).
*/
public boolean abutsWorkdays(LocalDateInterval other) {
return nextWorkday(getTomDato()).equals(adjustWeekendToMonday(other.getFomDato())) || nextWorkday(other.getTomDato()).equals(adjustWeekendToMonday(getFomDato()));
}

@Override
public int compareTo(LocalDateInterval periode) {
return ORDER_INTERVALS.compare(this, periode);
Expand Down Expand Up @@ -159,7 +171,11 @@ public NavigableSet<LocalDateInterval> except(LocalDateInterval annen) {
}

public LocalDateInterval expand(LocalDateInterval other) {
if (!(this.abuts(other) || this.overlaps(other))) {
return expand(other, LocalDateInterval::abuts);
}

public LocalDateInterval expand(LocalDateInterval other, BiPredicate<LocalDateInterval, LocalDateInterval> abuts) {
if (!(abuts.test(this, other) || this.overlaps(other))) {
throw new IllegalArgumentException(String.format("Intervals do not abut/overlap: %s <-> %s", this, other)); //$NON-NLS-1$
} else {
return new LocalDateInterval(min(this.getFomDato(), other.getFomDato()), max(getTomDato(), other.getTomDato()));
Expand Down Expand Up @@ -197,6 +213,10 @@ public boolean isConnected(LocalDateInterval other) {
return overlaps(other) || abuts(other);
}

public boolean isConnected(LocalDateInterval other, BiPredicate<LocalDateInterval, LocalDateInterval> abuts) {
return overlaps(other) || abuts.test(this, other);
}

public boolean isDateAfterOrEqualStartOfInterval(ChronoLocalDate dato) {
return getFomDato().isBefore(dato) || getFomDato().isEqual(dato);
}
Expand Down Expand Up @@ -285,4 +305,37 @@ public static LocalDateInterval parseFrom(String fom, String tom) {
LocalDate tomDato = tom == null || tom.isEmpty() || OPEN_END_FORMAT.equals(tom) ? null : LocalDate.parse(tom); // $NON-NLS-1$
return new LocalDateInterval(fomDato, tomDato);
}

/**
* Litt hjelp for perioder og arbeidsdager vs weekend. Funder på å inkludere helligdagsalgoritmen
*/
public static final Set<DayOfWeek> WEEKEND = Set.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);

public static LocalDate nextWorkday(LocalDate date) {
return adjustWeekendToMonday(date.plusDays(1));
}

public static LocalDate adjustWeekendToMonday(LocalDate date) {
return adjustWeekendTo(date, next(DayOfWeek.MONDAY));
}

public static LocalDate adjustWeekendToFriday(LocalDate date) {
return adjustWeekendTo(date, DayOfWeek.FRIDAY);
}

public static LocalDate adjustWeekendTo(LocalDate date, TemporalAdjuster weekendAdjuster) {
return WEEKEND.contains(DayOfWeek.from(date)) ? date.with(weekendAdjuster) : date;
}

public static LocalDate adjustThorughWeekend(LocalDate date) {
return WEEKEND.contains(DayOfWeek.from(date.plusDays(1))) ? date.with(DayOfWeek.SUNDAY) : date;
}

public LocalDateInterval adjustIntoWorkweek() {
return new LocalDateInterval(adjustWeekendToMonday(getFomDato()), adjustWeekendToFriday(getTomDato()));
}

public LocalDateInterval extendThroughWeekend() {
return new LocalDateInterval(getFomDato(), adjustThorughWeekend(getTomDato()));
}
}
45 changes: 36 additions & 9 deletions src/main/java/no/nav/fpsak/tidsserie/LocalDateTimeline.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ private static <X> LocalDateSegment<X> tilpassSegment(boolean harSegment, LocalD
* for å 'redusere' tidslinjen ned til enkleste form før lagring etc.
*/
public LocalDateTimeline<V> compress() {
var factory = new CompressorFactory<V>(Objects::equals, (i, lhs, rhs) -> new LocalDateSegment<>(i, lhs.getValue()));
var factory = new CompressorFactory<V>(LocalDateInterval::abuts, Objects::equals, StandardCombinators::leftOnly);
TimelineCompressor<V> compressor = segments.stream()
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);

Expand All @@ -290,13 +290,28 @@ public LocalDateTimeline<V> compress() {
* @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);
var factory = new CompressorFactory<>(LocalDateInterval::abuts, e, c);
TimelineCompressor<V> compressor = segments.stream()
.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 a - predikat for å vurdere om to intervaller ligger inntil hverandre (helg og helligdag)
* @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<LocalDateInterval, LocalDateInterval> a, BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
var factory = new CompressorFactory<>(a, e, c);
TimelineCompressor<V> compressor = segments.stream()
.collect(factory::get, TimelineCompressor::accept, TimelineCompressor::combine);

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

/**
* Returnerer timeline der enten denne eller andre (eller begge) har verdier.
*/
Expand Down Expand Up @@ -479,7 +494,11 @@ public <T, R> LocalDateTimeline<R> union(LocalDateTimeline<T> other, final Local
* @return true if continuous
*/
public boolean isContinuous() {
return firstDiscontinuity() == null;
return isContinuous(LocalDateInterval::abuts);
}

public boolean isContinuous(BiPredicate<LocalDateInterval, LocalDateInterval> abuts) {
return firstDiscontinuity(abuts) == null;
}

/**
Expand All @@ -488,14 +507,18 @@ public boolean isContinuous() {
* @return null if continuous, first interval
*/
public LocalDateInterval firstDiscontinuity() {
return firstDiscontinuity(LocalDateInterval::abuts);
}

public LocalDateInterval firstDiscontinuity(BiPredicate<LocalDateInterval, LocalDateInterval> abuts) {
if (segments.size() == 1) {
return null;
}

LocalDateInterval lastInterval = null;
for (LocalDateSegment<V> entry : segments) {
if (lastInterval != null) {
if (!lastInterval.abuts(entry.getLocalDateInterval())) {
if (!abuts.test(lastInterval, entry.getLocalDateInterval())) {
return new LocalDateInterval(lastInterval.getTomDato().plusDays(1), entry.getLocalDateInterval().getFomDato().minusDays(1));
}
}
Expand Down Expand Up @@ -981,10 +1004,12 @@ public LocalDateSegment<V> apply(LocalDateInterval di, LocalDateSegment<V> seg)
static class TimelineCompressor<V> implements Consumer<LocalDateSegment<V>> {

private final NavigableSet<LocalDateSegment<V>> segmenter = new TreeSet<>();
private final BiPredicate<LocalDateInterval, LocalDateInterval> abuts;
private final BiPredicate<V, V> equals;
private final LocalDateSegmentCombinator<V, V, V> combinator;

TimelineCompressor(BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
TimelineCompressor(BiPredicate<LocalDateInterval, LocalDateInterval> a, BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
this.abuts = a;
this.equals = e;
this.combinator = c;
}
Expand All @@ -995,11 +1020,11 @@ public void accept(LocalDateSegment<V> t) {
segmenter.add(t);
} else {
LocalDateSegment<V> last = segmenter.last();
if (last.getLocalDateInterval().abuts(t.getLocalDateInterval())
if (abuts.test(last.getLocalDateInterval(), t.getLocalDateInterval())
&& equals.test(last.getValue(), t.getValue())) {
// bytt ut og ekspander intervall for siste
segmenter.remove(last);
LocalDateInterval expandedInterval = last.getLocalDateInterval().expand(t.getLocalDateInterval());
LocalDateInterval expandedInterval = last.getLocalDateInterval().expand(t.getLocalDateInterval(), abuts);
segmenter.add(new LocalDateSegment<>(expandedInterval, combinator.combine(expandedInterval, last, t).getValue()));
} else {
segmenter.add(t);
Expand All @@ -1014,16 +1039,18 @@ public void combine(@SuppressWarnings("unused") TimelineCompressor<V> other) {
}

static class CompressorFactory<V> {
private final BiPredicate<LocalDateInterval, LocalDateInterval> abuts;
private final BiPredicate<V, V> equals;
private final LocalDateSegmentCombinator<V, V, V> combinator;

CompressorFactory(BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
CompressorFactory(BiPredicate<LocalDateInterval, LocalDateInterval> a, BiPredicate<V, V> e, LocalDateSegmentCombinator<V, V, V> c) {
this.abuts = a;
this.equals = e;
this.combinator = c;
}

TimelineCompressor<V> get() {
return new TimelineCompressor<>(equals, combinator);
return new TimelineCompressor<>(abuts, equals, combinator);
}
}

Expand Down
41 changes: 41 additions & 0 deletions src/test/java/no/nav/fpsak/tidsserie/LocalDateTimelineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
Expand Down Expand Up @@ -245,6 +246,46 @@ void skal_ikke_compresse_en_tidsserie_uten_sammenhengende_intervaller_med_samme_

}

@Test
void skal_compress_en_tidsserie_med_arbeidsuke_intervaller_med_samme_verdi() {

LocalDate d1 = LocalDate.now().with(DayOfWeek.WEDNESDAY);
LocalDate d2 = d1.with(DayOfWeek.FRIDAY);
LocalDate d3 = d1.plusWeeks(1).with(DayOfWeek.MONDAY);
LocalDate d4 = d3.plusDays(2);

// Arrange
LocalDateSegment<String> ds1 = new LocalDateSegment<>(d1, d2, "hello");
LocalDateSegment<String> ds2 = new LocalDateSegment<>(d3, d4, "hello");

// Assert adjusting
assertThat(new LocalDateInterval(d1, d2.plusDays(2)).adjustIntoWorkweek()).isEqualTo(ds1.getLocalDateInterval());
assertThat(new LocalDateInterval(d1, d2).extendThroughWeekend().getTomDato()).isEqualTo(d2.with(DayOfWeek.SUNDAY));
assertThat(new LocalDateInterval(d3.minusDays(1), d4).adjustIntoWorkweek()).isEqualTo(ds2.getLocalDateInterval());

// Arrange continuous
LocalDateTimeline<String> timeline = new LocalDateTimeline<>(List.of(ds1, ds2));

// Act continuous
var compressedTimeline = timeline.compress(LocalDateInterval::abutsWorkdays, String::equals, StandardCombinators::leftOnly);

// Assert continuous
assertThat(compressedTimeline.size()).isEqualTo(1);
assertThat(compressedTimeline).isEqualTo(new LocalDateTimeline<>(d1, d4, "hello"));

// Arrange discontinuous
LocalDateSegment<String> ds3 = new LocalDateSegment<>(d3.plusDays(1), d4, "hello");
LocalDateTimeline<String> discontinuous = new LocalDateTimeline<>(List.of(ds1, ds3));

// Act discontinuous
var compressedDiscont = discontinuous.compress(LocalDateInterval::abutsWorkdays, String::equals, StandardCombinators::leftOnly);

// Assert continuous
assertThat(compressedDiscont).isEqualTo(discontinuous);

}


@Test
void skal_gruppere_per_segment_periode() {
LocalDate d1 = LocalDate.now();
Expand Down

0 comments on commit 7f4d80e

Please sign in to comment.