diff --git a/pom.xml b/pom.xml
index e7bf479e..7a726fb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,7 @@
valutakurs-klient
unleash
metrikker
+ tidslinje
diff --git a/tidslinje/pom.xml b/tidslinje/pom.xml
new file mode 100644
index 00000000..745d9854
--- /dev/null
+++ b/tidslinje/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+
+ no.nav.familie.felles
+ felles
+ ${revision}${sha1}${changelist}
+
+
+ tidslinje
+ ${revision}${sha1}${changelist}
+ Felles - Tidslinje
+ jar
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+
+
+
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Periode.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Periode.kt
new file mode 100644
index 00000000..3b03e541
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Periode.kt
@@ -0,0 +1,31 @@
+@file:Suppress("UNCHECKED_CAST")
+
+package no.nav.familie.tidslinje
+
+import java.time.LocalDate
+
+data class Periode(
+ // NB: Generiske klasser arver type fra "Any?", så verdi kan være null.
+ val verdi: T,
+ val fom: LocalDate?,
+ val tom: LocalDate?,
+) {
+ fun tilTidslinjePeriodeMedDato() = TidslinjePeriodeMedDato(verdi, fom, tom)
+}
+
+fun List>.tilTidslinje(): Tidslinje =
+ this
+ .map { it.tilTidslinjePeriodeMedDato() }
+ .sortedBy { it.fom }
+ .tilTidslinje()
+
+fun List>.filtrerIkkeNull(): List> =
+ this.mapNotNull { periode -> periode.verdi?.let { periode as Periode } }
+
+fun List>.verdier(): List = this.map { it.verdi }
+
+data class IkkeNullbarPeriode(
+ val verdi: T,
+ val fom: LocalDate,
+ val tom: LocalDate,
+)
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Tidslinje.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Tidslinje.kt
new file mode 100644
index 00000000..8a096747
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Tidslinje.kt
@@ -0,0 +1,207 @@
+package no.nav.familie.tidslinje
+
+import no.nav.familie.tidslinje.utvidelser.klipp
+import no.nav.familie.tidslinje.utvidelser.kombinerMed
+import no.nav.familie.tidslinje.utvidelser.map
+import no.nav.familie.tidslinje.utvidelser.mapper
+import no.nav.familie.tidslinje.utvidelser.tilPerioder
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.temporal.TemporalAdjusters
+
+enum class TidsEnhet {
+ DAG,
+ UKE,
+ MÅNED,
+ ÅR,
+}
+
+/**
+ * En tidslinje består av ulike verdier over tid. Det vil si at en tidslinje kan ha en verdi
+ * A over et tidsintervall og en annen verdi B over et senere tidsintervall. Disse tidsintervallene blir håndert av Periode-klassen.
+ * En tidslinje kan visualiseres som en liste [A, A, A, A, ... , A, B, B, ... , B, C, C, ... , C] og et startstidspunkt,
+ * hvor A, B og C er ulike verdier og hver plass i lista representerer en dag.
+ * Tidslinjer kan innholde uendelige perioder. Det kan ikke være flere perioder med andre verdier etter en uendelig periode.
+ * En tidslinje støtter verdier av typen [Udefinert], [Null] og [PeriodeVerdi]. En verdi er udefinert når vi ikke vet
+ * hva verdien skal være (et hull i tidslinja). En verdi er no.nav.familie.tidslinje.Null når vi vet at det ikke finnes noe verdi i dette tidsrommet.
+ */
+open class Tidslinje(
+ var startsTidspunkt: LocalDate,
+ perioder: List>,
+ var tidsEnhet: TidsEnhet = TidsEnhet.DAG,
+) {
+ var innhold: List> = emptyList()
+ set(verdi) {
+ field = this.lagInnholdBasertPåPeriodelengder(verdi)
+ }
+ val foreldre: MutableList> = mutableListOf()
+ var tittel = ""
+
+ init {
+ this.innhold = perioder
+ startsTidspunkt =
+ when (tidsEnhet) {
+ TidsEnhet.ÅR -> startsTidspunkt.withDayOfYear(1)
+ TidsEnhet.MÅNED -> startsTidspunkt.withDayOfMonth(1)
+ TidsEnhet.UKE -> startsTidspunkt.with(DayOfWeek.MONDAY)
+ TidsEnhet.DAG -> startsTidspunkt
+ }
+ }
+
+ fun erTom() = innhold.sumOf { it.lengde } == 0
+
+ /**
+ * Kalkulerer slutttidspunkt som en LocalDate.
+ * Funksjonen returnerer den siste dagen som er med i tidslinja
+ * Om tidslinja er uendelig, kastes det et unntak
+ */
+ fun kalkulerSluttTidspunkt(): LocalDate {
+ val antallTidsEnheter: Int = this.innhold.sumOf { it.lengde }
+ val sluttTidspunkt = this.startsTidspunkt.plus(antallTidsEnheter.toLong() - 1, mapper[this.tidsEnhet])
+
+ return when (this.tidsEnhet) {
+ TidsEnhet.ÅR -> sluttTidspunkt.with(TemporalAdjusters.lastDayOfYear())
+ TidsEnhet.MÅNED -> sluttTidspunkt.with(TemporalAdjusters.lastDayOfMonth())
+ TidsEnhet.UKE -> sluttTidspunkt.with(DayOfWeek.SUNDAY)
+ TidsEnhet.DAG -> sluttTidspunkt
+ }
+ }
+
+ private fun kalkulerSluttTidspunkt(sluttDato: LocalDate): LocalDate =
+ when (this.tidsEnhet) {
+ TidsEnhet.ÅR -> sluttDato.with(TemporalAdjusters.lastDayOfYear())
+ TidsEnhet.MÅNED -> sluttDato.with(TemporalAdjusters.lastDayOfMonth())
+ TidsEnhet.UKE -> sluttDato.with(DayOfWeek.SUNDAY)
+ TidsEnhet.DAG -> sluttDato
+ }
+
+ override fun toString(): String =
+ "StartTidspunkt: " + startsTidspunkt + " Tidsenhet: " + tidsEnhet +
+ " Total lengde: " + innhold.sumOf { it.lengde } +
+ " Perioder: " +
+ innhold.mapIndexed { indeks, it ->
+ "(Verdi: " + it.periodeVerdi.verdi.toString() +
+ ", fom: " +
+ startsTidspunkt.plus(
+ innhold.take(indeks).sumOf { it.lengde }.toLong(),
+ mapper[this.tidsEnhet],
+ ) +
+ ", tom:" +
+ kalkulerSluttTidspunkt(
+ startsTidspunkt.plus(
+ innhold.take(indeks).sumOf { it.lengde }.toLong() + it.lengde - 1,
+ mapper[this.tidsEnhet],
+ ),
+ ) +
+ ")"
+ }
+
+ private fun lagInnholdBasertPåPeriodelengder(innhold: List>): List> {
+ val arr = mutableListOf>()
+
+ var i = 0
+ while (i < innhold.size) {
+ var j = i + 1
+ var lengde = innhold[i].lengde
+
+ while (j < innhold.size && innhold[i].periodeVerdi == innhold[j].periodeVerdi) {
+ lengde += innhold[j].lengde
+ j++
+ if (j >= innhold.size) break
+ }
+
+ val tidslinjePeriode =
+ TidslinjePeriode(
+ periodeVerdi = innhold[i].periodeVerdi,
+ lengde = lengde,
+ erUendelig = false,
+ )
+ i = j
+ arr.add(tidslinjePeriode)
+ if (tidslinjePeriode.erUendelig) return arr.toList()
+ }
+
+ return arr.toList()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is Tidslinje<*>) return false
+ return other.startsTidspunkt == this.startsTidspunkt &&
+ other.innhold == this.innhold &&
+ other.tidsEnhet == this.tidsEnhet &&
+ other.tittel == this.tittel
+ }
+
+ override fun hashCode(): Int =
+ this.startsTidspunkt.hashCode() + this.innhold.hashCode() + this.tidsEnhet.hashCode() + this.tittel.hashCode()
+
+ companion object
+}
+
+fun tomTidslinje(
+ startsTidspunkt: LocalDate? = null,
+ tidsEnhet: TidsEnhet = TidsEnhet.DAG,
+): Tidslinje = Tidslinje(startsTidspunkt = startsTidspunkt ?: PRAKTISK_TIDLIGSTE_DAG, emptyList(), tidsEnhet)
+
+fun Map>.leftJoin(
+ høyreTidslinjer: Map>,
+ kombinator: (V?, H?) -> R?,
+): Map> {
+ val venstreTidslinjer = this
+ val venstreNøkler = venstreTidslinjer.keys
+
+ return venstreNøkler.associateWith { nøkkel ->
+ val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, tomTidslinje())
+ val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, tomTidslinje())
+
+ venstreTidslinje.kombinerMed(høyreTidslinje, kombinator)
+ }
+}
+
+fun Map>.outerJoin(
+ høyreTidslinjer: Map>,
+ kombinator: (V?, H?) -> R?,
+): Map> {
+ val venstreTidslinjer = this
+ val alleNøkler = venstreTidslinjer.keys + høyreTidslinjer.keys
+
+ return alleNøkler.associateWith { nøkkel ->
+ val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, tomTidslinje())
+ val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, tomTidslinje())
+
+ venstreTidslinje.kombinerMed(høyreTidslinje, kombinator)
+ }
+}
+
+fun Map>.outerJoin(
+ tidslinjer2: Map>,
+ tidslinjer3: Map>,
+ kombinator: (A?, B?, C?) -> R?,
+): Map> {
+ val tidslinjer1 = this
+ val alleNøkler = tidslinjer1.keys + tidslinjer2.keys + tidslinjer3.keys
+
+ return alleNøkler.associateWith { nøkkel ->
+ val tidslinje1 = tidslinjer1.getOrDefault(nøkkel, tomTidslinje())
+ val tidslinje2 = tidslinjer2.getOrDefault(nøkkel, tomTidslinje())
+ val tidslinje3 = tidslinjer3.getOrDefault(nøkkel, tomTidslinje())
+
+ tidslinje1.kombinerMed(tidslinje2, tidslinje3, kombinator)
+ }
+}
+
+fun Tidslinje.beskjærEtter(tidslinje: Tidslinje<*>): Tidslinje =
+ this.klipp(tidslinje.startsTidspunkt, tidslinje.kalkulerSluttTidspunkt())
+
+fun Tidslinje.inneholder(verdi: T): Boolean = this.tilPerioder().any { it.verdi == verdi }
+
+fun Tidslinje.mapVerdi(mapper: (T?) -> R): Tidslinje =
+ this.map { periodeVerdi ->
+ when (periodeVerdi) {
+ is Verdi,
+ is Null,
+ -> mapper(periodeVerdi.verdi)?.let { Verdi(it) } ?: Null()
+
+ is Udefinert -> Udefinert()
+ }
+ }
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriode.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriode.kt
new file mode 100644
index 00000000..50b150fe
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriode.kt
@@ -0,0 +1,78 @@
+package no.nav.familie.tidslinje
+
+const val INF = 1_000_000_000
+
+sealed class PeriodeVerdi(
+ protected val _verdi: T?,
+) {
+ override operator fun equals(other: Any?): Boolean {
+ if (other !is PeriodeVerdi<*>) return false
+ if (other._verdi == this._verdi) return true
+ return false
+ }
+
+ override fun hashCode(): Int = this._verdi.hashCode()
+
+ abstract val verdi: T?
+}
+
+class Verdi(
+ override val verdi: T & Any,
+) : PeriodeVerdi(verdi)
+
+class Udefinert : PeriodeVerdi(null) {
+ override fun equals(other: Any?): Boolean = other is Udefinert<*>
+
+ override fun hashCode(): Int = this._verdi.hashCode()
+
+ override val verdi: T? = this._verdi
+}
+
+class Null : PeriodeVerdi(null) {
+ override fun equals(other: Any?): Boolean = other is Null<*>
+
+ override fun hashCode(): Int = this._verdi.hashCode()
+
+ override val verdi: T? = this._verdi
+}
+
+/**
+ * En periode representerer et tidsintervall hvor en tidslinje har en konstant verdi.
+ * En periode varer en tid [lengde], og kan være uendelig.
+ * Om [lengde] > [INF] eller [erUendelig] er satt til true, behandles perioden som at den har uendelig lengde.
+ * En tidslinje støtter verdier av typen [Udefinert], [Null] og [PeriodeVerdi]. En verdi er udefinert når vi ikke vet
+ * hva verdien skal være (et hull i tidslinja). En verdi er no.nav.familie.tidslinje.Null når vi vet at det ikke finnes en verdi i dette tidsrommet.
+ */
+data class TidslinjePeriode(
+ val periodeVerdi: PeriodeVerdi,
+ var lengde: Int,
+ var erUendelig: Boolean = false,
+) {
+ init {
+ if (lengde >= INF) {
+ erUendelig = true
+ }
+ if (erUendelig && lengde < INF) {
+ lengde = INF
+ }
+ if (lengde <= 0) {
+ throw java.lang.IllegalArgumentException("lengde må være større enn null.")
+ }
+ }
+
+ constructor(periodeVerdi: T?, lengde: Int, erUendelig: Boolean = false) : this(
+ if (periodeVerdi == null) {
+ Null()
+ } else {
+ Verdi(
+ periodeVerdi,
+ )
+ },
+ lengde,
+ erUendelig,
+ )
+
+ override fun toString(): String = "Verdi: " + periodeVerdi.verdi.toString() + ", Lengde: " + lengde
+}
+
+fun T?.tilPeriodeVerdi(): PeriodeVerdi = this?.let { Verdi(it) } ?: Null()
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDato.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDato.kt
new file mode 100644
index 00000000..64317e13
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDato.kt
@@ -0,0 +1,109 @@
+package no.nav.familie.tidslinje
+
+import java.time.LocalDate
+
+data class TidslinjePeriodeMedDato(
+ val periodeVerdi: PeriodeVerdi,
+ val fom: Dato,
+ val tom: Dato,
+) {
+ constructor(
+ verdi: T?,
+ fom: LocalDate?,
+ tom: LocalDate?,
+ ) : this(
+ periodeVerdi = verdi?.let { Verdi(it) } ?: Null(),
+ fom = Dato(fom ?: PRAKTISK_TIDLIGSTE_DAG),
+ tom = Dato(tom ?: PRAKTISK_SENESTE_DAG),
+ )
+
+ class Dato(
+ private val dato: LocalDate,
+ ) : Comparable {
+ fun tilLocalDateEllerNull(): LocalDate? =
+ if (this.dato == PRAKTISK_TIDLIGSTE_DAG || this.dato == PRAKTISK_SENESTE_DAG) {
+ null
+ } else {
+ this.dato
+ }
+
+ override fun compareTo(other: Dato): Int = this.dato.compareTo(other.dato)
+ }
+
+ fun tilPeriode() = Periode(periodeVerdi.verdi, fom.tilLocalDateEllerNull(), tom.tilLocalDateEllerNull())
+}
+
+fun List>.tilTidslinje(): Tidslinje {
+ val perioder = this.tilTidslinjePerioder()
+ return Tidslinje(
+ startsTidspunkt = this.firstOrNull()?.fom?.tilDatoEllerPraktiskTidligsteDag() ?: PRAKTISK_TIDLIGSTE_DAG,
+ perioder = perioder,
+ )
+}
+
+private fun List>.fyllInnTommePerioder(): List> =
+ this.fold(emptyList()) { periodeListeMedTommePerioder, periode ->
+ val sisteElement = periodeListeMedTommePerioder.lastOrNull()
+
+ if (sisteElement == null) {
+ periodeListeMedTommePerioder + periode
+ } else if (sisteElement.tom.tilDatoEllerPraktiskSenesteDag() ==
+ periode.fom
+ .tilDatoEllerPraktiskTidligsteDag()
+ .minusDays(1)
+ ) {
+ periodeListeMedTommePerioder + periode
+ } else {
+ periodeListeMedTommePerioder +
+ TidslinjePeriodeMedDato(
+ Udefinert(),
+ TidslinjePeriodeMedDato.Dato(sisteElement.tom.tilDatoEllerPraktiskSenesteDag().plusDays(1)),
+ TidslinjePeriodeMedDato.Dato(periode.fom.tilDatoEllerPraktiskTidligsteDag().minusDays(1)),
+ ) +
+ periode
+ }
+ }
+
+private fun List>.tilTidslinjePerioder(): List> {
+ this.validerKunFørsteEllerSistePeriodeErUendelig()
+ this.validerIngenOverlapp()
+
+ return this
+ .sortedBy { it.fom }
+ .fyllInnTommePerioder()
+ .map {
+ TidslinjePeriode(
+ periodeVerdi = it.periodeVerdi,
+ lengde = it.fom.tilDatoEllerPraktiskTidligsteDag().diffIDager(it.tom.tilDatoEllerPraktiskSenesteDag()),
+ erUendelig = it.tom.tilLocalDateEllerNull() == null,
+ )
+ }
+}
+
+fun List>.validerIngenOverlapp(feilmelding: String = "Feil med tidslinje. Overlapp på periode") {
+ this
+ .sortedBy { it.fom }
+ .zipWithNext { a, b ->
+ if (a.tom.tilDatoEllerPraktiskSenesteDag().isAfter(b.fom.tilDatoEllerPraktiskTidligsteDag())) {
+ error(message = feilmelding)
+ }
+ }
+}
+
+private fun List>.validerKunFørsteEllerSistePeriodeErUendelig() {
+ val sortertListe = this.sortedBy { it.fom }
+
+ sortertListe.forEachIndexed { indeks, periode ->
+ if (indeks != 0 && periode.fom.tilLocalDateEllerNull() == null) {
+ error("Feil med tidslinje. Flere perioder med fom=null")
+ }
+ if (indeks != sortertListe.lastIndex && periode.tom.tilLocalDateEllerNull() == null) {
+ error("Feil med tidslinje. Periode som ikke kommer på slutten har tom=null")
+ }
+ }
+}
+
+private fun TidslinjePeriodeMedDato.Dato.tilDatoEllerPraktiskTidligsteDag(): LocalDate =
+ this.tilLocalDateEllerNull() ?: PRAKTISK_TIDLIGSTE_DAG
+
+private fun TidslinjePeriodeMedDato.Dato.tilDatoEllerPraktiskSenesteDag(): LocalDate = this.tilLocalDateEllerNull() ?: PRAKTISK_SENESTE_DAG
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Utils.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Utils.kt
new file mode 100644
index 00000000..8a81ca4b
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Utils.kt
@@ -0,0 +1,14 @@
+package no.nav.familie.tidslinje
+
+import java.time.Duration
+import java.time.LocalDate
+
+val PRAKTISK_TIDLIGSTE_DAG = LocalDate.of(0, 1, 1)
+val PRAKTISK_SENESTE_DAG = LocalDate.MAX.minusYears(1)
+
+fun LocalDate.diffIDager(annen: LocalDate): Int =
+ Duration
+ // legger på én dag på sluttdatoen siden den er exlusive
+ .between(this.atStartOfDay(), annen.plusDays(1).atStartOfDay())
+ .toDaysPart()
+ .toInt()
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/FiltrerTidslinjer.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/FiltrerTidslinjer.kt
new file mode 100644
index 00000000..348a41da
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/FiltrerTidslinjer.kt
@@ -0,0 +1,16 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Null
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.Verdi
+
+fun Tidslinje.filtrer(predicate: (T?) -> Boolean) =
+ this.map {
+ when (it) {
+ is Verdi, is Null -> if (predicate(it.verdi)) it else Udefinert()
+ is Udefinert -> Udefinert()
+ }
+ }
+
+fun Tidslinje.filtrerIkkeNull(): Tidslinje = filtrer { it != null }
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjer.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjer.kt
new file mode 100644
index 00000000..ad375cc5
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjer.kt
@@ -0,0 +1,49 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Null
+import no.nav.familie.tidslinje.PRAKTISK_TIDLIGSTE_DAG
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.Verdi
+import no.nav.familie.tidslinje.tomTidslinje
+
+fun Collection>.slåSammen(): Tidslinje> {
+ val minsteTidspunkt = this.minOfOrNull { it.startsTidspunkt } ?: PRAKTISK_TIDLIGSTE_DAG
+ return this.fold(tomTidslinje(startsTidspunkt = minsteTidspunkt)) { sammenlagt, neste ->
+ sammenlagt.biFunksjon(neste) { periodeVerdiFraSammenlagt, periodeVerdiFraNeste ->
+ when (periodeVerdiFraSammenlagt) {
+ is Verdi ->
+ when (periodeVerdiFraNeste) {
+ is Verdi -> Verdi(periodeVerdiFraSammenlagt.verdi + periodeVerdiFraNeste.verdi)
+ else -> periodeVerdiFraSammenlagt
+ }
+
+ is Null ->
+ when (periodeVerdiFraNeste) {
+ is Verdi -> Verdi(listOf(periodeVerdiFraNeste.verdi))
+ else -> Null()
+ }
+
+ is Udefinert ->
+ when (periodeVerdiFraNeste) {
+ is Verdi -> Verdi(listOf(periodeVerdiFraNeste.verdi))
+ is Null -> Null()
+ is Udefinert -> Udefinert()
+ }
+ }
+ }
+ }
+}
+
+fun Collection>.kombiner(listeKombinator: (Iterable) -> R): Tidslinje =
+ this.slåSammen().map {
+ when (it) {
+ is Verdi -> {
+ val resultat = listeKombinator(it.verdi)
+ if (resultat != null) Verdi(resultat) else Null()
+ }
+
+ is Null -> Null()
+ is Udefinert -> Udefinert()
+ }
+ }
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/PeriodeUtvidelser.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/PeriodeUtvidelser.kt
new file mode 100644
index 00000000..500121a5
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/PeriodeUtvidelser.kt
@@ -0,0 +1,35 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.PeriodeVerdi
+import no.nav.familie.tidslinje.TidslinjePeriode
+
+/**
+ * Beregner en ny periode ved bruk av [operator] basert på periodene this og [operand].
+ * Den nye perioden har lengde [lengde] og verdien blir beregnet ut ifra å benytte seg av
+ * funksjonen [operator].
+ * [operator] vil få verdiene som de to input periodene består av som input.
+ */
+fun TidslinjePeriode.biFunksjon(
+ operand: TidslinjePeriode,
+ lengde: Int,
+ erUendelig: Boolean,
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): TidslinjePeriode = TidslinjePeriode(operator(this.periodeVerdi, operand.periodeVerdi), lengde, erUendelig)
+
+fun List>.slåSammenLike(): List> =
+ this.fold(emptyList()) { acc, tidslinjePeriode ->
+ val sisteElementIAcc = acc.lastOrNull()
+
+ if (sisteElementIAcc == null) {
+ listOf(tidslinjePeriode)
+ } else if (sisteElementIAcc.periodeVerdi == tidslinjePeriode.periodeVerdi) {
+ acc.dropLast(1) +
+ TidslinjePeriode(
+ periodeVerdi = sisteElementIAcc.periodeVerdi,
+ lengde = sisteElementIAcc.lengde + tidslinjePeriode.lengde,
+ erUendelig = sisteElementIAcc.erUendelig || tidslinjePeriode.erUendelig,
+ )
+ } else {
+ acc + tidslinjePeriode
+ }
+ }
diff --git a/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeUtvidelser.kt b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeUtvidelser.kt
new file mode 100644
index 00000000..c6d04aac
--- /dev/null
+++ b/tidslinje/src/main/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeUtvidelser.kt
@@ -0,0 +1,736 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.INF
+import no.nav.familie.tidslinje.Null
+import no.nav.familie.tidslinje.PRAKTISK_SENESTE_DAG
+import no.nav.familie.tidslinje.Periode
+import no.nav.familie.tidslinje.PeriodeVerdi
+import no.nav.familie.tidslinje.TidsEnhet
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import no.nav.familie.tidslinje.TidslinjePeriodeMedDato
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.filtrerIkkeNull
+import no.nav.familie.tidslinje.tilPeriodeVerdi
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
+import kotlin.math.absoluteValue
+
+val TIDENES_ENDE = LocalDate.MAX
+
+val mapper =
+ mapOf(
+ TidsEnhet.ÅR to ChronoUnit.YEARS,
+ TidsEnhet.MÅNED to ChronoUnit.MONTHS,
+ TidsEnhet.UKE to ChronoUnit.WEEKS,
+ TidsEnhet.DAG to ChronoUnit.DAYS,
+ )
+
+fun List>.join(
+ operand: Tidslinje,
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): List> = this.mapIndexed { _, tidslinjeBarn -> tidslinjeBarn.biFunksjon(operand, kombineringsfunksjon = operator) }
+
+fun List>.join(
+ operand: List>,
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): List> {
+ if (this.size != operand.size) throw IllegalArgumentException("Listene må ha lik lengde")
+ return this.mapIndexed { index, tidslinjeBarn ->
+ tidslinjeBarn.biFunksjon(
+ operand[index],
+ kombineringsfunksjon = operator,
+ )
+ }
+}
+
+fun Tidslinje.medTittel(tittel: String): Tidslinje {
+ this.tittel = tittel
+ return this
+}
+
+/**
+ * Konverterer to input-tidslinjer til å bli av samme lengde. Dette gjør den ved å legge til en "padding" bestående av en periode
+ * med lengde lik differansen mellom de to tidspunktene og verdi gitt av [nullVerdi] til tidslinjen.
+ * Antar tidslinjene er av samme tidsenhet!!
+ */
+private fun konverterTilSammeLengde(
+ tidslinje1: Tidslinje,
+ tidslinje2: Tidslinje,
+): Pair, Tidslinje> {
+ val kopi1 = Tidslinje(tidslinje1.startsTidspunkt, tidslinje1.innhold, tidsEnhet = tidslinje1.tidsEnhet)
+ val kopi2 = Tidslinje(tidslinje2.startsTidspunkt, tidslinje2.innhold, tidsEnhet = tidslinje2.tidsEnhet)
+
+ kopi1.tittel = tidslinje1.tittel
+ kopi2.tittel = tidslinje2.tittel
+
+ kopi1.foreldre.addAll(tidslinje1.foreldre)
+ kopi2.foreldre.addAll(tidslinje2.foreldre)
+
+ val udefinert1 = Udefinert()
+ val udefinert2 = Udefinert()
+
+ var tidsenhetForskjell =
+ kopi1.startsTidspunkt
+ .until(kopi2.startsTidspunkt, mapper[kopi1.tidsEnhet])
+ .toInt()
+ .absoluteValue
+
+ if (kopi1.startsTidspunkt > kopi2.startsTidspunkt) {
+ kopi1.innhold = listOf(TidslinjePeriode(udefinert1, tidsenhetForskjell, false)) + kopi1.innhold
+ kopi1.startsTidspunkt = kopi2.startsTidspunkt
+ } else if (kopi2.startsTidspunkt > kopi1.startsTidspunkt) {
+ kopi2.innhold = listOf(TidslinjePeriode(udefinert2, tidsenhetForskjell, false)) + kopi2.innhold
+ kopi2.startsTidspunkt = kopi1.startsTidspunkt
+ }
+
+ if (kopi1.kalkulerSluttTidspunkt() != kopi2.kalkulerSluttTidspunkt()) {
+ if (kopi1.innhold.isNotEmpty() && kopi1.innhold.last().erUendelig) {
+ kopi2.innhold = kopi2.innhold +
+ listOf(
+ TidslinjePeriode(
+ periodeVerdi = udefinert2,
+ lengde = INF,
+ erUendelig = true,
+ ),
+ )
+ return Pair(kopi1, kopi2)
+ } else if (kopi2.innhold.isNotEmpty() && kopi2.innhold.last().erUendelig) {
+ kopi1.innhold =
+ kopi1.innhold + listOf(TidslinjePeriode(periodeVerdi = udefinert1, lengde = INF, erUendelig = true))
+ return Pair(kopi1, kopi2)
+ }
+
+ if (kopi1.kalkulerSluttTidspunkt() < kopi2.kalkulerSluttTidspunkt()) {
+ tidsenhetForskjell =
+ kopi1
+ .kalkulerSluttTidspunkt()
+ .until(
+ kopi2.kalkulerSluttTidspunkt().plusDays(1),
+ mapper[kopi1.tidsEnhet],
+ ).toInt()
+ .absoluteValue
+ kopi1.innhold = kopi1.innhold + listOf(TidslinjePeriode(udefinert1, tidsenhetForskjell, false))
+ } else if (kopi2.kalkulerSluttTidspunkt() < kopi1.kalkulerSluttTidspunkt()) {
+ tidsenhetForskjell =
+ kopi1
+ .kalkulerSluttTidspunkt()
+ .plusDays(1)
+ .until(
+ kopi2.kalkulerSluttTidspunkt(),
+ mapper[kopi1.tidsEnhet],
+ ).toInt()
+ .absoluteValue
+ kopi2.innhold = kopi2.innhold + listOf(TidslinjePeriode(udefinert2, tidsenhetForskjell, false))
+ }
+ }
+
+ return Pair(kopi1, kopi2)
+}
+
+/**
+ * Tar inn en tidslinje av en vilkårlig tidsenhet (sålangt kan det kun være MÅNED eller DAG) og returnerer den som DAG.
+ */
+fun Tidslinje.konverterTilDag(): Tidslinje {
+ if (this.tidsEnhet == TidsEnhet.DAG) return this
+
+ var tidspunkt = this.startsTidspunkt
+
+ when (this.tidsEnhet) {
+ TidsEnhet.UKE -> {
+ for (periode in this.innhold) {
+ val nyttTidspunkt = tidspunkt.plusWeeks(periode.lengde.toLong())
+ periode.lengde = tidspunkt.until(nyttTidspunkt, ChronoUnit.DAYS).toInt()
+ tidspunkt = nyttTidspunkt
+ }
+ }
+
+ TidsEnhet.MÅNED -> {
+ for (periode in this.innhold) {
+ val nyttTidspunkt = tidspunkt.plusMonths(periode.lengde.toLong())
+ periode.lengde = tidspunkt.until(nyttTidspunkt, ChronoUnit.DAYS).toInt()
+ tidspunkt = nyttTidspunkt
+ }
+ }
+
+ else -> {
+ for (periode in this.innhold) {
+ val nyttTidspunkt = tidspunkt.plusYears(periode.lengde.toLong())
+ periode.lengde = tidspunkt.until(nyttTidspunkt, ChronoUnit.DAYS).toInt()
+ tidspunkt = nyttTidspunkt
+ }
+ }
+ }
+
+ this.tidsEnhet = TidsEnhet.DAG
+
+ return this
+}
+
+/**
+ * Tar inn en operator som bestemmer hvordan periodeverdiene i tidslinja skal mappes til andre periodeverdier.
+ * [operator] er en funksjon som tar inn en periodeverdi og returnerer en periodeverdi.
+ * Dette åpner for at man kan mappe no.nav.familie.tidslinje.Null og no.nav.familie.tidslinje.Udefinert objekter til andre verdier.
+ */
+fun Tidslinje.map(operator: (elem: PeriodeVerdi) -> PeriodeVerdi): Tidslinje {
+ val perioder = mutableListOf>()
+
+ this.innhold.forEach { perioder.add(TidslinjePeriode(operator(it.periodeVerdi), it.lengde, it.erUendelig)) }
+
+ val tidslinje = Tidslinje(this.startsTidspunkt, perioder, this.tidsEnhet)
+
+ tidslinje.foreldre.addAll(this.foreldre)
+
+ return tidslinje
+ .medTittel(this.tittel)
+}
+
+/**
+ * Fjerner alle periodeverdier i [periodeVerdier] fra slutten og begynnelsen av tidslinja.
+ * Ved fjerning av perioder i begynnelsen av tislinja, vil startspunktet bli flyttet.
+ */
+fun Tidslinje.trim(vararg periodeVerdier: PeriodeVerdi): Tidslinje =
+ this
+ .trimVenstre(*periodeVerdier)
+ .trimHøyre(*periodeVerdier)
+ .medTittel(this.tittel)
+
+fun Tidslinje.trimVenstre(vararg periodeVerdier: PeriodeVerdi): Tidslinje {
+ val perioder = ArrayList(this.innhold)
+ var startsTidspunkt = this.startsTidspunkt
+
+ for (periode in this.innhold) {
+ if (periodeVerdier.contains(periode.periodeVerdi)) {
+ startsTidspunkt = startsTidspunkt.plus(periode.lengde.toLong(), mapper[this.tidsEnhet])
+ perioder.remove(periode)
+ } else {
+ break
+ }
+ }
+ val resultat = Tidslinje(startsTidspunkt, perioder, this.tidsEnhet)
+
+ this.foreldre.forEach {
+ if (!resultat.foreldre.contains(it)) {
+ resultat.foreldre.add(it)
+ }
+ }
+
+ return resultat
+}
+
+fun Tidslinje.trimHøyre(vararg periodeVerdier: PeriodeVerdi): Tidslinje {
+ val perioder = ArrayList(this.innhold)
+
+ for (periode in this.innhold.reversed()) {
+ if (periodeVerdier.contains(periode.periodeVerdi)) {
+ perioder.remove(periode)
+ } else {
+ break
+ }
+ }
+
+ val resultat = Tidslinje(this.startsTidspunkt, perioder, this.tidsEnhet)
+
+ this.foreldre.forEach {
+ if (!resultat.foreldre.contains(it)) {
+ resultat.foreldre.add(it)
+ }
+ }
+
+ return resultat
+}
+
+/**
+ * Tar inn en tidslinje med tidsenhet DAG og returnerer en med tidsenhet MÅNED.
+ * Parameteren [vindu] bestemmer hvor mange måneder som skal bli tatt med i beregeningen av månedsverdien for gjeldene måned.
+ * Med vindu parameteren satt for man en sliding-window effekt med steglende en.
+ * Operator definerer hvordan verdiene for alle månedene skal bli valgt (f.eks. skal det tas gjennomsnitt av alle verdiene
+ * innad i den måneden, skal den største verdien velges eller skal den siste verdien velges).
+ * [dato] parameteren i operator oppdateres til å være første dag i gjeldene måned under beregningene.
+ */
+fun Tidslinje.konverterTilMåned(
+ antallMndBakoverITid: Int = 0,
+ antallMndFremoverITid: Int = 0,
+ operator: (dato: LocalDate, månedListe: List>>) -> PeriodeVerdi,
+): Tidslinje {
+ val listeAvMåneder: MutableList>> = this.splittPåMåned()
+
+ if (listeAvMåneder.size < antallMndBakoverITid + antallMndFremoverITid) {
+ throw java.lang.IllegalArgumentException("Det er for få månender i tidslinja for dette vinduet.")
+ }
+
+ listeAvMåneder.addAll(0, (0 until antallMndBakoverITid).map { emptyList() })
+ listeAvMåneder.addAll((0 until antallMndFremoverITid).map { emptyList() })
+
+ val perioder: MutableList> = mutableListOf()
+ var dato = this.startsTidspunkt.withDayOfMonth(1)
+
+ listeAvMåneder.windowed(size = antallMndBakoverITid + antallMndFremoverITid + 1, partialWindows = false) { vindu ->
+ perioder.add(TidslinjePeriode(operator(dato, vindu), 1, false))
+ dato = dato.plusMonths(1)
+ // dersom inneværende måned er uendelig, må man beregne verdien dersom vinduet kun dekker den uendelige periodeVerdien.
+ if (vindu[antallMndBakoverITid].last().erUendelig) {
+ val listeMedUendeligPeriodeVerdier =
+ (0..antallMndBakoverITid + antallMndFremoverITid).map { listOf(vindu[antallMndBakoverITid].last()) }
+ perioder.add(TidslinjePeriode(operator(dato, listeMedUendeligPeriodeVerdier), INF, true))
+ }
+ }
+
+ return Tidslinje(this.startsTidspunkt, perioder, TidsEnhet.MÅNED)
+ .medTittel(this.tittel)
+}
+
+/**
+ * Shifter en tidslinje [antall] tidsenheter (enten DAG eller MÅNED) mot høyre.
+ */
+fun Tidslinje.høyreShift(antall: Int = 1): Tidslinje =
+ Tidslinje(this.startsTidspunkt.plus(antall.toLong(), mapper[this.tidsEnhet]), this.innhold, this.tidsEnhet)
+ .medTittel(this.tittel)
+
+/**
+ * Splitter periodene i en tidslinje på månedsgrenser og returnerer en liste av
+ * lister med perioder der hver liste representerer en egen måned.
+ */
+fun Tidslinje.splittPåMåned(): MutableList>> {
+ var nåværendeMåned = this.startsTidspunkt
+ var antallDagerIgjenIMåned = this.startsTidspunkt.lengthOfMonth() - this.startsTidspunkt.dayOfMonth + 1
+
+ var månedListe: MutableList> = mutableListOf() // representerer periodene innad i en måned
+
+ val listeAvMåneder: MutableList>> =
+ mutableListOf() // liste av lister som representerer en måned
+
+ this.innhold.forEachIndexed { index, periode ->
+
+ if (periode.erUendelig) { // Her håndteres uendelige perioder
+ // om vi er midt i en måned må vi først legge til en ikke-uendelig periode som fyller opp denne
+ if (nåværendeMåned.lengthOfMonth() > antallDagerIgjenIMåned) {
+ månedListe.add(TidslinjePeriode(periode.periodeVerdi, antallDagerIgjenIMåned, false))
+ listeAvMåneder.add(månedListe)
+ månedListe = mutableListOf()
+ nåværendeMåned = nåværendeMåned.plusMonths(1)
+ }
+ månedListe.add(periode) // her adder vi den uendelige perioden
+ listeAvMåneder.add(månedListe) // om koden har kommet hit vil den være ferdig, da uendelige perioder alltid er bakerst
+ } else if (periode.lengde < antallDagerIgjenIMåned) { // Her håndteres perioder som går opp i inneværende måned
+ månedListe.add(periode)
+ antallDagerIgjenIMåned -= periode.lengde
+
+ if (index + 1 == this.innhold.size) { // om vi er på siste periode er vi ferdige og månedListe kan addes.
+ listeAvMåneder.add(månedListe)
+ }
+ } else { // Her håndteres perioder som er lenger enn inneværende måned og ikke uendelig
+ var lengde = periode.lengde // holder oversikt over hvor mange dager vi har igjen av en gitt periode
+
+ while (lengde > antallDagerIgjenIMåned) { // "Så lenge perioden har igjen flere dager enn det er dager igjen i måneden"
+ månedListe.add(TidslinjePeriode(periode.periodeVerdi, antallDagerIgjenIMåned))
+ listeAvMåneder.add(månedListe)
+ månedListe = mutableListOf()
+ lengde -= antallDagerIgjenIMåned
+ nåværendeMåned = nåværendeMåned.plusMonths(1)
+ antallDagerIgjenIMåned = nåværendeMåned.lengthOfMonth()
+ }
+ // kommer koden hit betyr det at gjenstående lengde til perioden er mindre enn antallDagerIgjenIMåned
+ if (lengde > 0) {
+ månedListe.add(TidslinjePeriode(periode.periodeVerdi, lengde))
+ antallDagerIgjenIMåned -= lengde
+
+ if (antallDagerIgjenIMåned == 0) {
+ listeAvMåneder.add(månedListe)
+ månedListe = mutableListOf()
+ nåværendeMåned = nåværendeMåned.plusMonths(1)
+ antallDagerIgjenIMåned = nåværendeMåned.lengthOfMonth()
+ } else if (index + 1 == this.innhold.size) { // om vi er på siste periode er vi ferdige og månedListe kan addes.
+ listeAvMåneder.add(månedListe)
+ }
+ }
+ }
+ }
+ return listeAvMåneder
+}
+
+private fun klippeOperator(
+ status1: PeriodeVerdi,
+ status2: PeriodeVerdi,
+): PeriodeVerdi =
+ if (status1 is Udefinert || status2 is Udefinert) {
+ Udefinert()
+ } else if (status1 is Null || status2 is Null) {
+ Null()
+ } else {
+ if (status2.verdi == true) {
+ status1
+ } else {
+ Udefinert()
+ }
+ }
+
+fun Tidslinje.klipp(
+ startsTidspunkt: LocalDate,
+ sluttTidspunkt: LocalDate,
+): Tidslinje {
+ val foreldre = this.foreldre
+
+ var resultat =
+ if (sluttTidspunkt.isAfter(startsTidspunkt)) {
+ val justertSluttTidspunkt =
+ if (sluttTidspunkt == TIDENES_ENDE) sluttTidspunkt else sluttTidspunkt.plusDays(1)
+
+ val tidslinjeKlipp =
+ Tidslinje(
+ startsTidspunkt,
+ listOf(
+ TidslinjePeriode(
+ true,
+ lengde =
+ startsTidspunkt
+ .until(
+ justertSluttTidspunkt,
+ mapper[this.tidsEnhet],
+ ).toInt(),
+ ),
+ ),
+ this.tidsEnhet,
+ )
+ this
+ .biFunksjon(tidslinjeKlipp) { status1, status2 ->
+ klippeOperator(
+ status1,
+ status2,
+ )
+ }.fjernForeldre()
+ } else {
+ Tidslinje(startsTidspunkt, emptyList(), this.tidsEnhet)
+ }
+
+ resultat = resultat.trim(Udefinert())
+
+ if (resultat.startsTidspunkt > sluttTidspunkt) {
+ resultat.startsTidspunkt = startsTidspunkt
+ }
+
+ resultat.foreldre.addAll(foreldre)
+ return resultat
+ .medTittel(this.tittel)
+}
+
+fun Tidslinje.biFunksjonSnitt(
+ operand: Tidslinje,
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): Tidslinje {
+ val startsTidspunkt =
+ if (this.startsTidspunkt < operand.startsTidspunkt) operand.startsTidspunkt else this.startsTidspunkt
+
+ val sluttTidspunkt1 = this.kalkulerSluttTidspunkt()
+ val sluttTidspunkt2 = operand.kalkulerSluttTidspunkt()
+
+ val sluttTidspunkt = if (sluttTidspunkt1 < sluttTidspunkt2) sluttTidspunkt1 else sluttTidspunkt2
+
+ return this
+ .biFunksjon(operand, operator)
+ .klipp(startsTidspunkt, sluttTidspunkt)
+}
+
+/**
+ * Beregner en ny tidslinje ved bruk av [kombineringsfunksjon] basert på tidslinjene this og [annen].
+ * Går gjennom alle periodene i hver tidslinje og beregner nye perioder.
+ * Hver nye periode som blir lagt inn i den resulterende tidslinja starter der den forrige stoppet og
+ * har lengde lik det minste tidsrommet hvor input tidslinjene har konstant verdi.
+ * Påfølgende perioder med lik verdi, blir slått sammen til en periode i den resulterende tidslinja.
+ * [kombineringsfunksjon] blir brukt for å beregne verdiene til de generete periodene basert på verdiene til input tidslinjene i
+ * de respektive tidsrommene. Om en av input-tidslinjene er uendleig(dvs at den siste perioden har uendelig varighet), vil
+ * også den resulterende tidslinja være uendelig.
+ * [PeriodeVerdi] er en wrapper-classe som blir brukt for å håndtere no.nav.familie.tidslinje.Udefinert og no.nav.familie.tidslinje.Null. [kombineringsfunksjon]
+ * må ta høyde for at input kan være av en av disse typene, og definere hvordan disse situasjonene håndteres.
+ * MERK: operator skal returnere enten Udefindert, no.nav.familie.tidslinje.Null eller no.nav.familie.tidslinje.PeriodeVerdi.
+ */
+fun Tidslinje.biFunksjon(
+ annen: Tidslinje,
+ kombineringsfunksjon: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): Tidslinje {
+ val lst: MutableList> = mutableListOf()
+ var kopi1 = this
+ var kopi2 = annen
+
+ if (this.tidsEnhet != annen.tidsEnhet) {
+ kopi1 = this.konverterTilDag()
+ kopi2 = annen.konverterTilDag()
+ }
+
+ val (kopi3, kopi4) = konverterTilSammeLengde(kopi1, kopi2)
+
+ val it1: Iterator> = kopi3.innhold.iterator()
+ val it2: Iterator> = kopi4.innhold.iterator()
+
+ var tmpTidslinjePeriode: TidslinjePeriode
+
+ var tidslinjePeriode1: TidslinjePeriode? = null
+ var tidslinjePeriode2: TidslinjePeriode? = null
+
+ var lengde1 = 0
+ var lengde2 = 0
+
+ while (it1.hasNext() || it2.hasNext()) {
+ tidslinjePeriode1 = tidslinjePeriode1 ?: it1.next()
+ tidslinjePeriode2 = tidslinjePeriode2 ?: it2.next()
+
+ if (tidslinjePeriode2.erUendelig && tidslinjePeriode1.erUendelig) {
+ lst.add(
+ tidslinjePeriode1.biFunksjon(
+ operand = tidslinjePeriode2,
+ lengde = INF,
+ erUendelig = true,
+ operator = kombineringsfunksjon,
+ ),
+ )
+ break
+ }
+
+ lengde1 = if (lengde1 <= 0) tidslinjePeriode1.lengde else lengde1
+ lengde2 = if (lengde2 <= 0) tidslinjePeriode2.lengde else lengde2
+
+ while (lengde1 > 0 && lengde2 > 0) {
+ if (lengde1 < lengde2) {
+ lengde2 -= lengde1
+ tmpTidslinjePeriode =
+ tidslinjePeriode1.biFunksjon(
+ operand = tidslinjePeriode2,
+ lengde = lengde1,
+ erUendelig = false,
+ operator = kombineringsfunksjon,
+ )
+ lengde1 = 0
+ } else {
+ lengde1 -= lengde2
+ tmpTidslinjePeriode =
+ tidslinjePeriode1.biFunksjon(
+ operand = tidslinjePeriode2,
+ lengde = lengde2,
+ erUendelig = false,
+ operator = kombineringsfunksjon,
+ )
+ lengde2 = 0
+ }
+ lst.add(tmpTidslinjePeriode)
+ }
+
+ if (lengde1 <= 0) {
+ tidslinjePeriode1 = null
+ }
+ if (lengde2 <= 0) {
+ tidslinjePeriode2 = null
+ }
+ }
+
+ val resultatTidslinje = Tidslinje(kopi3.startsTidspunkt, lst, kopi3.tidsEnhet)
+
+ @Suppress("UNCHECKED_CAST")
+ resultatTidslinje.foreldre.add(kopi3 as Tidslinje)
+ @Suppress("UNCHECKED_CAST")
+ resultatTidslinje.foreldre.add(kopi4 as Tidslinje)
+
+ return resultatTidslinje
+ .medTittel(this.tittel)
+}
+
+/**
+ * Beregner en ny tidslinje ved bruk av [operator] basert på tidslinjene this og [operand].
+ * Går gjennom alle periodene i hver tidslinje og beregner nye perioder.
+ * Hver nye periode som blir lagt inn i den resulterende tidslinja starter der den forrige stoppet og
+ * har lengde lik det minste tidsrommet hvor input tidslinjene har konstant verdi.
+ * Påfølgende perioder med lik verdi, blir slått sammen til en periode i den resulterende tidslinja.
+ * [Operator] blir brukt for å beregne verdiene til de generete periodene basert på verdiene til input tidslinjene i
+ * de respektive tidsrommene. Om en av input-tidslinjene er uendleig(dvs at den siste perioden har uendelig varighet), vil
+ * også den resulterende tidslinja være uendelig.
+ * Denne metoden krever at de to input tidslinjene og den resulterende tidslinja består av samme type verdier T.
+ */
+fun Tidslinje.binærOperator(
+ operand: Tidslinje,
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): Tidslinje = this.biFunksjon(operand, operator)
+
+/**
+ * Lager en ny tidslengde fra en streng [innhold] og en mapper [mapper].
+ * Mappere må inneholde en mapping for enhver char i strengen til et annet objekt av typen [R].
+ * [tidsEnhet] spesifiserer hvor lang tid hver char skal vare.
+ * Periodene i den resulterende tidslinja vil ha lengder angitt i dager.
+ * Tidslunja starter på [startsTidspunkt]
+ */
+fun Tidslinje.Companion.lagTidslinjeFraStreng(
+ innhold: String,
+ startDato: LocalDate,
+ mapper: Map,
+ tidsEnhet: TidsEnhet,
+): Tidslinje {
+ val lst: MutableList> = mutableListOf()
+
+ innhold.forEach {
+ if (mapper[it] == null) {
+ throw java.lang.IllegalArgumentException("Kunne ikke mappe fra char $it til et objekt")
+ }
+ lst.add(TidslinjePeriode(mapper[it], 1, false))
+ }
+
+ return Tidslinje(startDato, lst, tidsEnhet)
+}
+
+/**
+ * Initialiserer en tidslinje fra listen [innhold].
+ * [tidsEnhet] spesifiserer hvor lange periodene for hvert element varer.
+ * Periodene i den resulterende tidslinja vil ha lengder angitt i dager.
+ * Tidslinja starter på [startsTidspunkt] og behandler [nullVerdi] som nullverdi.
+ */
+fun Tidslinje.Companion.lagTidslinjeFraListe(
+ innhold: List,
+ startDato: LocalDate,
+ tidsEnhet: TidsEnhet,
+): Tidslinje {
+ val perioder =
+ innhold.map {
+ TidslinjePeriode(it, 1, false)
+ }
+
+ return Tidslinje(startDato, perioder, tidsEnhet = tidsEnhet)
+}
+
+/**
+ * Slår sammen en liste av tidslinjer med samme type ved å ta i bruk reduce. [operator] bestemmer regelen som skal brukes
+ * når man slår sammen to liste-elementer. [nullVerdi] må sendes inn fordi denne trengs av binærOperator.
+ */
+fun List>.slåSammenLikeTidslinjer(
+ operator: (elem1: PeriodeVerdi, elem2: PeriodeVerdi) -> PeriodeVerdi,
+): Tidslinje {
+ if (this.isEmpty()) {
+ throw java.lang.IllegalArgumentException("Lista kan ikke være tom")
+ }
+
+ val resultatTidslinje: Tidslinje
+
+ if (this.size == 1) {
+ resultatTidslinje = Tidslinje(this[0].startsTidspunkt, this[0].innhold, this[0].tidsEnhet)
+ @Suppress("UNCHECKED_CAST")
+ resultatTidslinje.foreldre.add(this[0] as Tidslinje)
+ } else {
+ resultatTidslinje =
+ this.reduce { t1, t2 -> t1.binærOperator(t2) { elem1, elem2 -> operator(elem1, elem2) } }.fjernForeldre()
+ resultatTidslinje.foreldre.addAll(this.filterIsInstance>())
+ }
+
+ return resultatTidslinje
+}
+
+fun Tidslinje.fjernForeldre(): Tidslinje {
+ this.foreldre.clear()
+ return this
+}
+
+fun Tidslinje.hentVerdier(): List = this.innhold.slåSammenLike().map { it.periodeVerdi.verdi }
+
+/**
+ * Summerer opp tiden for hver periode og legger inn i en TidslinjePeriodeMedDato
+ *
+ * Om vi har en tidslinje med starttidspunkt 1. januar og tre perioder som varer én månde hver med verdiene a, b og c
+ * vil resulatet bli:
+ *
+ * List(TidslinjePeriodeMedDato(
+ * verdi: a
+ * fom: 1. januar
+ * tom: 31. januar
+ * )
+ * TidslinjePeriodeMedDato(
+ * verdi: b
+ * fom: 1. februar
+ * tom: 28. februar
+ * )
+ * TidslinjePeriodeMedDato(
+ * verdi: c
+ * fom: 1. mars
+ * tom: 31. mars
+ * ))
+ *
+ */
+fun Tidslinje.tilTidslinjePerioderMedDato(): List> {
+ val (tidslinjePeriodeMedLocalDateListe, _) =
+ this.innhold.fold(Pair(emptyList>(), 0L)) {
+ (
+ tidslinjePeriodeMedLocalDateListe: List>,
+ tidFraStarttidspunktFom: Long,
+ ),
+ tidslinjePeriode,
+ ->
+ val tidFraStarttidspunktTilNesteFom = tidFraStarttidspunktFom + tidslinjePeriode.lengde
+
+ Pair(
+ tidslinjePeriodeMedLocalDateListe +
+ TidslinjePeriodeMedDato(
+ periodeVerdi = tidslinjePeriode.periodeVerdi,
+ fom =
+ TidslinjePeriodeMedDato.Dato(
+ this.startsTidspunkt.leggTil(
+ tidsEnhet,
+ tidFraStarttidspunktFom,
+ ),
+ ),
+ tom =
+ if (tidslinjePeriode.erUendelig) {
+ TidslinjePeriodeMedDato.Dato(PRAKTISK_SENESTE_DAG)
+ } else {
+ TidslinjePeriodeMedDato.Dato(
+ this.startsTidspunkt
+ .leggTil(tidsEnhet, tidFraStarttidspunktTilNesteFom)
+ .minusDays(1),
+ )
+ },
+ ),
+ tidFraStarttidspunktTilNesteFom,
+ )
+ }
+ return tidslinjePeriodeMedLocalDateListe
+}
+
+private fun LocalDate.leggTil(
+ tidsEnhet: TidsEnhet,
+ antall: Long,
+): LocalDate =
+ when (tidsEnhet) {
+ TidsEnhet.DAG -> this.plusDays(antall)
+ TidsEnhet.UKE -> this.plusWeeks(antall)
+ TidsEnhet.MÅNED -> this.plusMonths(antall)
+ TidsEnhet.ÅR -> this.plusYears(antall)
+ }
+
+fun Tidslinje.kombinerMed(
+ annen: Tidslinje,
+ kombineringsfunksjon: (elem1: T?, elem2: R?) -> RESULTAT?,
+): Tidslinje =
+ this.biFunksjon(annen) { periodeverdiVenstre, periodeverdiHøyre ->
+ kombineringsfunksjon(periodeverdiVenstre.verdi, periodeverdiHøyre.verdi)
+ .tilPeriodeVerdi()
+ }
+
+fun Tidslinje.kombinerMed(
+ tidslinje2: Tidslinje,
+ tidslinje3: Tidslinje,
+ kombineringsfunksjon: (elem1: T?, elem2: R?, elem3: S?) -> RESULTAT?,
+): Tidslinje {
+ val tidslinje1Og2: Tidslinje> =
+ this.biFunksjon(tidslinje2) { elem1PeriodeVerdi, elem2PeriodeVerdi ->
+ Pair(elem1PeriodeVerdi.verdi, elem2PeriodeVerdi.verdi).tilPeriodeVerdi()
+ }
+
+ return tidslinje1Og2.biFunksjon(tidslinje3) { elem1Og2, elem3PeriodeVerdi ->
+ val (elem1, elem2) = elem1Og2.verdi ?: Pair(null, null)
+ kombineringsfunksjon(elem1, elem2, elem3PeriodeVerdi.verdi).tilPeriodeVerdi()
+ }
+}
+
+fun Tidslinje.tilPerioder(): List> = this.tilTidslinjePerioderMedDato().map { it.tilPeriode() }
+
+fun Tidslinje.tilPerioderIkkeNull(): List> = this.tilPerioder().filtrerIkkeNull()
+
+fun Tidslinje.slåSammenLikePerioder(): Tidslinje =
+ Tidslinje(
+ startsTidspunkt = this.startsTidspunkt,
+ perioder = this.innhold.slåSammenLike(),
+ tidsEnhet = this.tidsEnhet,
+ )
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/PeriodeTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/PeriodeTest.kt
new file mode 100644
index 00000000..f2e94922
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/PeriodeTest.kt
@@ -0,0 +1,161 @@
+package no.nav.familie.tidslinje
+
+import no.nav.familie.tidslinje.utvidelser.kombinerMed
+import no.nav.familie.tidslinje.utvidelser.tilPerioder
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class PeriodeTest {
+ private val førsteJanuar = LocalDate.of(2022, 1, 1)
+ private val sisteDagIJanuar = LocalDate.of(2022, 1, 31)
+ private val førsteFebruar = LocalDate.of(2022, 2, 1)
+ private val sisteDagIFebruar = LocalDate.of(2022, 2, 28)
+ private val førsteMars = LocalDate.of(2022, 3, 1)
+ private val sisteDagIMars = LocalDate.of(2022, 3, 31)
+ private val førsteApril = LocalDate.of(2022, 4, 1)
+ private val sisteDagIApril = LocalDate.of(2022, 4, 30)
+
+ @Test
+ fun `tilPerioder - Skal beholde datoer ved manipulering på tidslinje`() {
+ val tidslinjeA = listOf(Periode("a", førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val tidslinjeB = listOf(Periode("b", førsteFebruar, sisteDagIFebruar)).tilTidslinje()
+
+ val periode =
+ tidslinjeA
+ .kombinerMed(tidslinjeB) { a, b ->
+ b ?: a
+ }.tilPerioder()
+
+ Assertions.assertEquals(3, periode.size)
+
+ Assertions.assertEquals(førsteJanuar, periode[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, periode[0].tom)
+
+ Assertions.assertEquals(førsteFebruar, periode[1].fom)
+ Assertions.assertEquals(sisteDagIFebruar, periode[1].tom)
+
+ Assertions.assertEquals(førsteMars, periode[2].fom)
+ Assertions.assertEquals(sisteDagIMars, periode[2].tom)
+ }
+
+ @Test
+ fun `tilPerioder - skal finne fjernet perioder fra tidslinjer`() {
+ val tidslinjeA = listOf(Periode("a", førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val tidslinjeB =
+ listOf(
+ Periode("b", førsteJanuar, sisteDagIJanuar),
+ Periode("b", førsteMars, sisteDagIMars),
+ ).tilTidslinje()
+
+ val periode = tidslinjeA.kombinerMed(tidslinjeB) { v1, v2 -> if (v2 == null) v1 else null }.tilPerioder().filtrerIkkeNull()
+
+ Assertions.assertEquals(1, periode.size)
+
+ Assertions.assertEquals(førsteFebruar, periode[0].fom)
+ Assertions.assertEquals(sisteDagIFebruar, periode[0].tom)
+ }
+
+ @Test
+ fun `tilPerioder - skal finne lagret perioder fra tidslinjer`() {
+ val tidslinjeA = listOf(Periode("a", førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val tidslinjeB = listOf(Periode("b", førsteJanuar, sisteDagIApril)).tilTidslinje()
+
+ val periode = tidslinjeB.kombinerMed(tidslinjeA) { v1, v2 -> if (v2 == null) v1 else null }.tilPerioder().filtrerIkkeNull()
+
+ Assertions.assertEquals(1, periode.size)
+
+ Assertions.assertEquals(førsteApril, periode[0].fom)
+ Assertions.assertEquals(sisteDagIApril, periode[0].tom)
+ }
+
+ @Test
+ fun `tilPerioder - Skal kunne håndtere splitt i tidslinje`() {
+ val perioder =
+ listOf(
+ Periode("a", førsteJanuar, sisteDagIJanuar),
+ Periode("c", førsteMars, sisteDagIMars),
+ ).tilTidslinje().tilPerioder()
+
+ Assertions.assertEquals(3, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, perioder[0].tom)
+ Assertions.assertEquals("a", perioder[0].verdi)
+
+ Assertions.assertEquals(førsteFebruar, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIFebruar, perioder[1].tom)
+ Assertions.assertEquals(null, perioder[1].verdi)
+
+ Assertions.assertEquals(førsteMars, perioder[2].fom)
+ Assertions.assertEquals(sisteDagIMars, perioder[2].tom)
+ Assertions.assertEquals("c", perioder[2].verdi)
+ }
+
+ @Test
+ fun `tilPerioder - Skal kunne håndtere nullverdier i starten og slutten av tidslinje`() {
+ val perioder =
+ listOf(
+ Periode("a", null, sisteDagIJanuar),
+ Periode("b", førsteFebruar, sisteDagIFebruar),
+ Periode("c", førsteMars, null),
+ ).tilTidslinje().tilPerioder()
+
+ Assertions.assertEquals(3, perioder.size)
+
+ Assertions.assertEquals(null, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, perioder[0].tom)
+ Assertions.assertEquals("a", perioder[0].verdi)
+
+ Assertions.assertEquals(førsteFebruar, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIFebruar, perioder[1].tom)
+ Assertions.assertEquals("b", perioder[1].verdi)
+
+ Assertions.assertEquals(førsteMars, perioder[2].fom)
+ Assertions.assertEquals(null, perioder[2].tom)
+ Assertions.assertEquals("c", perioder[2].verdi)
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil dersom det er flere tom-datoer med nullverdi`() {
+ val perioder =
+ listOf(
+ Periode("a", null, sisteDagIJanuar),
+ Periode("b", førsteFebruar, null),
+ Periode("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { perioder.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil dersom det er flere fom-datoer med nullverdi`() {
+ val perioder =
+ listOf(
+ Periode("a", null, sisteDagIJanuar),
+ Periode("b", null, sisteDagIFebruar),
+ Periode("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { perioder.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil om det er overlapp i periodene`() {
+ val perioder =
+ listOf(
+ Periode("a", null, sisteDagIJanuar),
+ Periode("b", førsteFebruar, sisteDagIMars),
+ Periode("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { perioder.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje og tilPerioder - Skal håndtere tom liste`() {
+ val perioder = emptyList>()
+
+ Assertions.assertEquals(0, perioder.tilTidslinje().tilPerioder().size)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDatoTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDatoTest.kt
new file mode 100644
index 00000000..6c3c20f8
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/TidslinjePeriodeMedDatoTest.kt
@@ -0,0 +1,139 @@
+package no.nav.familie.tidslinje
+
+import no.nav.familie.tidslinje.utvidelser.biFunksjon
+import no.nav.familie.tidslinje.utvidelser.tilTidslinjePerioderMedDato
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class TidslinjePeriodeMedDatoTest {
+ private val førsteJanuar = LocalDate.of(2022, 1, 1)
+ private val sisteDagIJanuar = LocalDate.of(2022, 1, 31)
+ private val førsteFebruar = LocalDate.of(2022, 2, 1)
+ private val sisteDagIFebruar = LocalDate.of(2022, 2, 28)
+ private val førsteMars = LocalDate.of(2022, 3, 1)
+ private val sisteDagIMars = LocalDate.of(2022, 3, 31)
+
+ @Test
+ fun `tilTidslinjePerioderMedDato - Skal beholde datoer ved manipulering på tidslinje`() {
+ val tidslinjeA = listOf(TidslinjePeriodeMedDato("a", førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val tidslinjeB = listOf(TidslinjePeriodeMedDato("b", førsteFebruar, sisteDagIFebruar)).tilTidslinje()
+
+ val tidslinjePerioderMedDato =
+ tidslinjeA
+ .biFunksjon(tidslinjeB) { a, b ->
+ if (b is Verdi) {
+ b
+ } else {
+ a
+ }
+ }.tilTidslinjePerioderMedDato()
+
+ Assertions.assertEquals(3, tidslinjePerioderMedDato.size)
+
+ Assertions.assertEquals(førsteJanuar, tidslinjePerioderMedDato[0].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIJanuar, tidslinjePerioderMedDato[0].tom.tilLocalDateEllerNull())
+
+ Assertions.assertEquals(førsteFebruar, tidslinjePerioderMedDato[1].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIFebruar, tidslinjePerioderMedDato[1].tom.tilLocalDateEllerNull())
+
+ Assertions.assertEquals(førsteMars, tidslinjePerioderMedDato[2].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIMars, tidslinjePerioderMedDato[2].tom.tilLocalDateEllerNull())
+ }
+
+ @Test
+ fun `tilTidslinjePerioderMedDato - Skal kunne håndtere splitt i tidslinje`() {
+ val tidslinjePerioderMedDato =
+ listOf(
+ TidslinjePeriodeMedDato("a", førsteJanuar, sisteDagIJanuar),
+ TidslinjePeriodeMedDato("c", førsteMars, sisteDagIMars),
+ ).tilTidslinje().tilTidslinjePerioderMedDato()
+
+ Assertions.assertEquals(3, tidslinjePerioderMedDato.size)
+
+ Assertions.assertEquals(førsteJanuar, tidslinjePerioderMedDato[0].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIJanuar, tidslinjePerioderMedDato[0].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Verdi::class.java, tidslinjePerioderMedDato[0].periodeVerdi::class.java)
+ Assertions.assertEquals("a", tidslinjePerioderMedDato[0].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteFebruar, tidslinjePerioderMedDato[1].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIFebruar, tidslinjePerioderMedDato[1].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Udefinert::class.java, tidslinjePerioderMedDato[1].periodeVerdi::class.java)
+ Assertions.assertEquals(null, tidslinjePerioderMedDato[1].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteMars, tidslinjePerioderMedDato[2].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIMars, tidslinjePerioderMedDato[2].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Verdi::class.java, tidslinjePerioderMedDato[2].periodeVerdi::class.java)
+ Assertions.assertEquals("c", tidslinjePerioderMedDato[2].periodeVerdi.verdi)
+ }
+
+ @Test
+ fun `tilTidslinjePerioderMedDato - Skal kunne håndtere nullverdier i starten og slutten av tidslinje`() {
+ val tidslinjePerioderMedDato =
+ listOf(
+ TidslinjePeriodeMedDato("a", null, sisteDagIJanuar),
+ TidslinjePeriodeMedDato("b", førsteFebruar, sisteDagIFebruar),
+ TidslinjePeriodeMedDato("c", førsteMars, null),
+ ).tilTidslinje().tilTidslinjePerioderMedDato()
+
+ Assertions.assertEquals(3, tidslinjePerioderMedDato.size)
+
+ Assertions.assertEquals(null, tidslinjePerioderMedDato[0].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIJanuar, tidslinjePerioderMedDato[0].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Verdi::class.java, tidslinjePerioderMedDato[0].periodeVerdi::class.java)
+ Assertions.assertEquals("a", tidslinjePerioderMedDato[0].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteFebruar, tidslinjePerioderMedDato[1].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIFebruar, tidslinjePerioderMedDato[1].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Verdi::class.java, tidslinjePerioderMedDato[1].periodeVerdi::class.java)
+ Assertions.assertEquals("b", tidslinjePerioderMedDato[1].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteMars, tidslinjePerioderMedDato[2].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(null, tidslinjePerioderMedDato[2].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Verdi::class.java, tidslinjePerioderMedDato[2].periodeVerdi::class.java)
+ Assertions.assertEquals("c", tidslinjePerioderMedDato[2].periodeVerdi.verdi)
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil dersom det er flere tom-datoer med nullverdi`() {
+ val tidslinjePerioderMedDato =
+ listOf(
+ TidslinjePeriodeMedDato("a", null, sisteDagIJanuar),
+ TidslinjePeriodeMedDato("b", førsteFebruar, null),
+ TidslinjePeriodeMedDato("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { tidslinjePerioderMedDato.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil dersom det er flere fom-datoer med nullverdi`() {
+ val tidslinjePerioderMedDato =
+ listOf(
+ TidslinjePeriodeMedDato("a", null, sisteDagIJanuar),
+ TidslinjePeriodeMedDato("b", null, sisteDagIFebruar),
+ TidslinjePeriodeMedDato("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { tidslinjePerioderMedDato.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje - Skal kaste feil om det er overlapp i periodene`() {
+ val tidslinjePerioderMedDato =
+ listOf(
+ TidslinjePeriodeMedDato("a", null, sisteDagIJanuar),
+ TidslinjePeriodeMedDato("b", førsteFebruar, sisteDagIMars),
+ TidslinjePeriodeMedDato("c", førsteMars, null),
+ )
+
+ Assertions.assertThrows(Exception::class.java) { tidslinjePerioderMedDato.tilTidslinje() }
+ }
+
+ @Test
+ fun `tilTidslinje og tilTidslinjePerioderMedDato - Skal håndtere tom liste`() {
+ val tidslinjePerioderMedDato = emptyList>()
+
+ Assertions.assertEquals(0, tidslinjePerioderMedDato.tilTidslinje().tilTidslinjePerioderMedDato().size)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerMedTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerMedTest.kt
new file mode 100644
index 00000000..a959b789
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerMedTest.kt
@@ -0,0 +1,85 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Periode
+import no.nav.familie.tidslinje.tilTidslinje
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class KombinerMedTest {
+ private val førsteJanuar = LocalDate.of(2022, 1, 1)
+ private val sisteDagIJanuar = LocalDate.of(2022, 1, 31)
+ private val førsteFebruar = LocalDate.of(2022, 2, 1)
+ private val sisteDagIFebruar = LocalDate.of(2022, 2, 28)
+ private val førsteMars = LocalDate.of(2022, 3, 1)
+ private val sisteDagIMars = LocalDate.of(2022, 3, 31)
+ private val førsteApril = LocalDate.of(2022, 4, 1)
+ private val sisteDagIApril = LocalDate.of(2022, 4, 30)
+
+ /**
+ * a = |111-|
+ * b = |-2-2|
+ * (a ?: 0) + (b ?: 0) = |1312|
+ **/
+ @Test
+ fun `kombinerMed - Skal kombinere overlappende verdier på tidslinjene`() {
+ val tidslinjeA = listOf(Periode(1, førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val tidslinjeB =
+ listOf(Periode(2, førsteFebruar, sisteDagIFebruar), Periode(2, førsteApril, sisteDagIApril)).tilTidslinje()
+
+ val perioder =
+ tidslinjeA
+ .kombinerMed(tidslinjeB) { verdiFraTidslinjeA, verdiFraTidslinjeB ->
+ (verdiFraTidslinjeA ?: 0) + (verdiFraTidslinjeB ?: 0)
+ }.tilPerioder()
+
+ Assertions.assertEquals(4, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, perioder[0].tom)
+ Assertions.assertEquals(1, perioder[0].verdi)
+
+ Assertions.assertEquals(førsteFebruar, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIFebruar, perioder[1].tom)
+ Assertions.assertEquals(3, perioder[1].verdi)
+
+ Assertions.assertEquals(førsteMars, perioder[2].fom)
+ Assertions.assertEquals(sisteDagIMars, perioder[2].tom)
+ Assertions.assertEquals(1, perioder[2].verdi)
+
+ Assertions.assertEquals(førsteApril, perioder[3].fom)
+ Assertions.assertEquals(sisteDagIApril, perioder[3].tom)
+ Assertions.assertEquals(2, perioder[3].verdi)
+ }
+
+ /**
+ * a = |1--|
+ * b = |--2|
+ * (a ?: 0) + (b ?: 0) = |102|
+ **/
+ @Test
+ fun `kombinerMed - Skal ikke kombinere verdier som ikke overlapper`() {
+ val tidslinjeA = listOf(Periode(1, førsteJanuar, sisteDagIJanuar)).tilTidslinje()
+ val tidslinjeB = listOf(Periode(2, førsteMars, sisteDagIMars)).tilTidslinje()
+
+ val perioder =
+ tidslinjeA
+ .kombinerMed(tidslinjeB) { verdiFraTidslinjeA, verdiFraTidslinjeB ->
+ (verdiFraTidslinjeA ?: 0) + (verdiFraTidslinjeB ?: 0)
+ }.tilPerioder()
+
+ Assertions.assertEquals(3, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, perioder[0].tom)
+ Assertions.assertEquals(1, perioder[0].verdi)
+
+ Assertions.assertEquals(førsteFebruar, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIFebruar, perioder[1].tom)
+ Assertions.assertEquals(0, perioder[1].verdi)
+
+ Assertions.assertEquals(førsteMars, perioder[2].fom)
+ Assertions.assertEquals(sisteDagIMars, perioder[2].tom)
+ Assertions.assertEquals(2, perioder[2].verdi)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjerTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjerTest.kt
new file mode 100644
index 00000000..9b99cab5
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KombinerTidslinjerTest.kt
@@ -0,0 +1,62 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Null
+import no.nav.familie.tidslinje.TidslinjePeriodeMedDato
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.tilTidslinje
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class KombinerTidslinjerTest {
+ private val førsteJanuar = LocalDate.of(2022, 1, 1)
+ private val sisteDagIJanuar = LocalDate.of(2022, 1, 31)
+ private val førsteFebruar = LocalDate.of(2022, 2, 1)
+ private val sisteDagIFebruar = LocalDate.of(2022, 2, 28)
+ private val førsteMars = LocalDate.of(2022, 3, 1)
+ private val sisteDagIMars = LocalDate.of(2022, 3, 31)
+ private val førsteApril = LocalDate.of(2022, 4, 1)
+ private val sisteDagIApril = LocalDate.of(2022, 4, 30)
+ private val førsteMai = LocalDate.of(2022, 5, 1)
+ private val sisteDagIMai = LocalDate.of(2022, 5, 31)
+
+ @Test
+ fun `kombinerTidslinjer - skal kunne kombinere liste av tidslinjer til én tilslinje med lister som verdier`() {
+ val tidslinjeA = listOf(TidslinjePeriodeMedDato("a", førsteJanuar, sisteDagIJanuar)).tilTidslinje()
+ val tidslinjeB = listOf(TidslinjePeriodeMedDato("b", førsteJanuar, sisteDagIFebruar)).tilTidslinje()
+ val tidslinjeC = listOf(TidslinjePeriodeMedDato("c", førsteJanuar, sisteDagIMars)).tilTidslinje()
+ val nullTidlisline =
+ listOf(
+ TidslinjePeriodeMedDato(
+ Null(),
+ TidslinjePeriodeMedDato.Dato(førsteMai),
+ TidslinjePeriodeMedDato.Dato(sisteDagIMai),
+ ),
+ ).tilTidslinje()
+
+ val kombinerteTidslinjerPerioder =
+ listOf(tidslinjeA, tidslinjeB, tidslinjeC, nullTidlisline).slåSammen().tilTidslinjePerioderMedDato()
+
+ Assertions.assertEquals(5, kombinerteTidslinjerPerioder.size)
+
+ Assertions.assertEquals(førsteJanuar, kombinerteTidslinjerPerioder[0].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIJanuar, kombinerteTidslinjerPerioder[0].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(listOf("a", "b", "c"), kombinerteTidslinjerPerioder[0].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteFebruar, kombinerteTidslinjerPerioder[1].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIFebruar, kombinerteTidslinjerPerioder[1].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(listOf("b", "c"), kombinerteTidslinjerPerioder[1].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteMars, kombinerteTidslinjerPerioder[2].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIMars, kombinerteTidslinjerPerioder[2].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(listOf("c"), kombinerteTidslinjerPerioder[2].periodeVerdi.verdi)
+
+ Assertions.assertEquals(førsteApril, kombinerteTidslinjerPerioder[3].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIApril, kombinerteTidslinjerPerioder[3].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Udefinert::class.java, kombinerteTidslinjerPerioder[3].periodeVerdi::class.java)
+
+ Assertions.assertEquals(førsteMai, kombinerteTidslinjerPerioder[4].fom.tilLocalDateEllerNull())
+ Assertions.assertEquals(sisteDagIMai, kombinerteTidslinjerPerioder[4].tom.tilLocalDateEllerNull())
+ Assertions.assertEquals(Null::class.java, kombinerteTidslinjerPerioder[4].periodeVerdi::class.java)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KonverterTidTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KonverterTidTest.kt
new file mode 100644
index 00000000..b7637fd0
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/KonverterTidTest.kt
@@ -0,0 +1,650 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.INF
+import no.nav.familie.tidslinje.TidsEnhet
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import no.nav.familie.tidslinje.Verdi
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
+
+class KonverterTidTest {
+ @Test
+ fun `Kan gå fra dager til måneder`() {
+ val dato1Start = LocalDate.of(2022, 7, 1)
+ val dato1Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato2Start = LocalDate.of(2022, 8, 1)
+ val dato2Slutt = LocalDate.of(2022, 8, 31)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().last().periodeVerdi }.høyreShift()
+ val correct = listOf(1, 2)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato2Start, tidslinjeMåned.startsTidspunkt)
+ }
+
+ @Test
+ fun `Kan gå fra dager til måneder test 2`() {
+ val dato1Start = LocalDate.of(2022, 7, 5)
+ val dato1Slutt = LocalDate.of(2022, 8, 4)
+
+ val dato2Start = LocalDate.of(2022, 8, 5)
+ val dato2Slutt = LocalDate.of(2022, 9, 3)
+
+ val dato3Start = LocalDate.of(2022, 9, 4)
+ val dato3Slutt = LocalDate.of(2022, 10, 31)
+
+ val dato4Start = LocalDate.of(2022, 11, 1)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().last().periodeVerdi }.høyreShift()
+ val correct = listOf(1, 2, 3, 4)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato2Start.withDayOfMonth(1), tidslinjeMåned.startsTidspunkt)
+ }
+
+ @Test
+ fun `kan konvertere til måneder når det er flere TidslinjePerioder i løpet av en måned`() {
+ val dato1Start = LocalDate.of(2022, 7, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 20)
+
+ val dato3Start = LocalDate.of(2022, 7, 21)
+ val dato3Slutt = LocalDate.of(2022, 10, 31)
+
+ val dato4Start = LocalDate.of(2022, 11, 1)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().last().periodeVerdi }.høyreShift()
+ val correct = listOf(3, 4)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato1Start.plusMonths(1).withDayOfMonth(1), tidslinjeMåned.startsTidspunkt)
+ }
+
+ @Test
+ fun `flere måneder med flere TidslinjePerioder i hver måned`() {
+ val dato1Start = LocalDate.of(2022, 7, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 8, 11)
+
+ val dato4Start = LocalDate.of(2022, 8, 12)
+ val dato4Slutt = LocalDate.of(2022, 8, 31)
+
+ val dato5Start = LocalDate.of(2022, 9, 1)
+ val dato5Slutt = LocalDate.of(2022, 9, 3)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(5, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().last().periodeVerdi }.høyreShift()
+ val correct = listOf(2, 4, 5)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato1Start.plusMonths(1).withDayOfMonth(1), tidslinjeMåned.startsTidspunkt)
+ }
+
+ @Test
+ fun `TidslinjePeriode går over flere måneder på slutten`() {
+ val dato1Start = LocalDate.of(2022, 7, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val dato4Start = LocalDate.of(2022, 10, 12)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val dato5Start = LocalDate.of(2022, 12, 1)
+ val dato5Slutt = LocalDate.of(2023, 3, 5)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(5, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.splittPåMåned()
+ val correct = listOf(2, 3, 3, 4, 4, 5, 5, 5, 5)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.map { it.last().periodeVerdi.verdi }.toList())
+ }
+
+ @Test
+ fun `TidslinjePeriode går over flere måneder på slutten gg`() {
+ val dato1Start = LocalDate.of(2022, 7, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val dato4Start = LocalDate.of(2022, 10, 12)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val dato5Start = LocalDate.of(2022, 12, 1)
+ val dato5Slutt = LocalDate.of(2023, 3, 5)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4.0, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(5.0, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned =
+ tidslinje.konverterTilMåned { _, it ->
+ Verdi(
+ it
+ .last()
+ .map { it.periodeVerdi.verdi!! }
+ .toList()
+ .average(),
+ )
+ }
+ val correct: List = listOf(1.5, 3.0, 3.5, 4.0, 5.0)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+ }
+
+ @Test
+ fun `kunne beregne månedlig valutakurs fra dagskurser`() {
+ val test =
+ Tidslinje.lagTidslinjeFraListe(
+ listOf(
+ 1.0,
+ 2.0,
+ 3.0,
+ 4.0,
+ 5.0,
+ 6.0,
+ 7.0,
+ 8.0,
+ 9.0,
+ 10.0,
+ 11.0,
+ 12.0,
+ 13.0,
+ 14.0,
+ 15.0,
+ 16.0,
+ 17.0,
+ 18.0,
+ 19.0,
+ 20.0,
+ 21.0,
+ 22.0,
+ 23.0,
+ 24.0,
+ 25.0,
+ 26.0,
+ 27.0,
+ 28.0,
+ 29.0,
+ 30.0,
+ 31.0,
+ 1.0,
+ 2.0,
+ 3.0,
+ 4.0,
+ 5.0,
+ 6.0,
+ 7.0,
+ 8.0,
+ 9.0,
+ 10.0,
+ 11.0,
+ 12.0,
+ 13.0,
+ 14.0,
+ 15.0,
+ 16.0,
+ 17.0,
+ 18.0,
+ 19.0,
+ 20.0,
+ 21.0,
+ 22.0,
+ 23.0,
+ 24.0,
+ 25.0,
+ 26.0,
+ 27.0,
+ 28.0,
+ 1.0,
+ 2.0,
+ 3.0,
+ 4.0,
+ 5.0,
+ 6.0,
+ 7.0,
+ 8.0,
+ 9.0,
+ 10.0,
+ 11.0,
+ 12.0,
+ 13.0,
+ 14.0,
+ 15.0,
+ 16.0,
+ 17.0,
+ 18.0,
+ 19.0,
+ 20.0,
+ 21.0,
+ 22.0,
+ 23.0,
+ 24.0,
+ 25.0,
+ 26.0,
+ 27.0,
+ 28.0,
+ 29.0,
+ 30.0,
+ 31.0,
+ ),
+ LocalDate.of(2022, 1, 1),
+ TidsEnhet.DAG,
+ )
+
+ val tidslinjeMåned =
+ test
+ .konverterTilMåned { _, it ->
+ Verdi(
+ it
+ .last()
+ .map { it.periodeVerdi.verdi!! }
+ .toList()
+ .average(),
+ )
+ }.høyreShift()
+ val correct = listOf(16.0, 14.5, 16.0)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(
+ LocalDate.of(2022, 1, 1).plusMonths(1).withDayOfMonth(1),
+ tidslinjeMåned.startsTidspunkt,
+ )
+ }
+
+ @Test
+ fun `tester med operatoren verdien som har vart lengst blir valgt for inneværende TidslinjePeriode`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val dato4Start = LocalDate.of(2022, 10, 12)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val dato5Start = LocalDate.of(2022, 12, 1)
+ val dato5Slutt = LocalDate.of(2023, 3, 5)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4.0, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(5.0, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val splittPåMåned = tidslinje.splittPåMåned()
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().maxBy { it.lengde }.periodeVerdi }
+
+ val correctBeforeTidslinje: List =
+ listOf(
+ 1.0,
+ 2.0,
+ 3.0,
+ 3.0,
+ 4.0,
+ 4.0,
+ 5.0,
+ 5.0,
+ 5.0,
+ 5.0,
+ ) // Dette er sånn lista skulle vært, men siden Tidslinje slår sammen like blir det
+ val correct: List = listOf(1.0, 2.0, 3.0, 4.0, 5.0)
+
+ // Assertions.assertEquals(tidslinjeMåned.innhold[2].lengde, (dato3Start.until(LocalDate.of(2022, 9, 30), ChronoUnit.DAYS).toInt() + 1))
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(
+ correctBeforeTidslinje,
+ splittPåMåned.map { it.maxBy { periode -> periode.lengde }.periodeVerdi.verdi },
+ )
+ }
+
+ @Test
+ fun `tester med boolske verdier`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val dato4Start = LocalDate.of(2022, 10, 12)
+ val dato4Slutt = LocalDate.of(2022, 11, 30)
+
+ val dato5Start = LocalDate.of(2022, 12, 1)
+ val dato5Slutt = LocalDate.of(2023, 3, 5)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(true, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(false, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(true, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(true, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(false, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned = tidslinje.konverterTilMåned { _, it -> it.last().last().periodeVerdi }
+
+ val correct: List =
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ ) // Dette er sånn lista skulle vært, men siden Tidslinje slår sammen like blir det
+
+ Assertions.assertEquals(tidslinjeMåned.innhold[2].lengde, 4)
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+ // Assertions.assertEquals(correctBeforeTidslinje, splittPåMåned.map { it.maxBy { it.lengde }.periodeVerdi.verdi })
+ }
+
+ @Test
+ fun `Kan håndtere tidslinjer med uendelig slutt`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val dato4Start = LocalDate.of(2022, 10, 12)
+ val dato4Slutt = LocalDate.of(2022, 11, 10)
+
+ val dato5Start = LocalDate.of(2022, 11, 11)
+ val dato5Slutt = LocalDate.of(2023, 3, 5)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(2.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(3.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(4.0, dato4Start.until(dato4Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(5.0, dato5Start.until(dato5Slutt, ChronoUnit.DAYS).toInt() + 1, true),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val splittPåMåned = tidslinje.splittPåMåned()
+
+ val tidslinjeMåned =
+ tidslinje.konverterTilMåned { _, it ->
+ it.last().maxBy { it.lengde }.periodeVerdi
+ } // velger den verdien som varer lengst innad i en måned.
+
+ val correctBeforeTidslinje: List =
+ listOf(
+ 1.0,
+ 2.0,
+ 3.0,
+ 3.0,
+ 4.0,
+ 5.0,
+ 5.0,
+ ) // Dette er sånn lista skulle vært, men siden Tidslinje slår sammen like blir det
+ val correct: List = listOf(1.0, 2.0, 3.0, 4.0, 5.0)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(1, tidslinjeMåned.innhold[3].lengde)
+ assertTrue { INF <= tidslinjeMåned.innhold[4].lengde }
+ Assertions.assertEquals(
+ correctBeforeTidslinje,
+ splittPåMåned.map { it.maxBy { periode -> periode.lengde }.periodeVerdi.verdi },
+ )
+ assertTrue(tidslinjeMåned.innhold.last().erUendelig)
+ }
+
+ @Test
+ fun `kan ta gjennomsnittet over to måneder for å bestemme verdien til en måned`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(5.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(7.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(15.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned =
+ tidslinje.konverterTilMåned(1) { _, vindu ->
+ if (vindu[0].isEmpty()) {
+ Verdi(vindu[1].last().periodeVerdi.verdi!!)
+ } else {
+ val avg = (vindu[0].last().periodeVerdi.verdi!! + vindu[1].last().periodeVerdi.verdi!!) / 2
+ Verdi(avg)
+ }
+ }
+
+ val correct: List = listOf(5, (5 + 7) / 2, (7 + 15) / 2, 15)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi!!.toInt() }.toList())
+ }
+
+ @Test
+ fun `kan spesifisere at januar skal få en annen verdi enn andre måneder`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(5.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(7.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(15.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ var tidslinjeMåned =
+ tidslinje.konverterTilMåned(2) { dato, _ ->
+ if (dato.month == java.time.Month.JULY) {
+ Verdi(1.0)
+ } else {
+ Verdi(
+ 0.0,
+ )
+ }
+ }
+
+ var correct = listOf(0.0, 1.0, 0.0)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi!! }.toList())
+
+ tidslinjeMåned =
+ tidslinje.konverterTilMåned(2) { dato, _ ->
+ if (dato.month == java.time.Month.OCTOBER) {
+ Verdi(1.0)
+ } else {
+ Verdi(
+ 0.0,
+ )
+ }
+ }
+
+ correct = listOf(0.0, 1.0)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi!! }.toList())
+ }
+
+ @Test
+ fun `kan kovertere til måned med window 1 på begge sider når siste mnd er uendelig`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(5.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(7.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(15.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1, erUendelig = true),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned =
+ tidslinje.konverterTilMåned(antallMndBakoverITid = 1, antallMndFremoverITid = 1) { _, vindu ->
+ if (vindu[0].isEmpty()) {
+ Verdi((vindu[1].last().periodeVerdi.verdi!! + vindu[2].last().periodeVerdi.verdi!!) / 2)
+ } else if (vindu[2].isEmpty()) {
+ Verdi((vindu[0].last().periodeVerdi.verdi!! + vindu[1].last().periodeVerdi.verdi!!) / 2)
+ } else {
+ val avg =
+ (
+ vindu[0].last().periodeVerdi.verdi!! +
+ vindu[1].last().periodeVerdi.verdi!! +
+ vindu[2].last().periodeVerdi.verdi!!
+ ) / 3
+ Verdi(avg)
+ }
+ }
+
+ val correct: List = listOf((5 + 7) / 2, (5 + 7 + 15) / 3, (7 + 15) / 2, 15)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi!!.toInt() }.toList())
+ }
+
+ @Test
+ fun `kan kovertere til måned med window kun ett element og uendelig ende`() {
+ val dato1Start = LocalDate.of(2022, 6, 5)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato3Start = LocalDate.of(2022, 8, 1)
+ val dato3Slutt = LocalDate.of(2022, 10, 11)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(5.0, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(7.0, dato2Start.until(dato2Slutt, ChronoUnit.DAYS).toInt() + 1),
+ TidslinjePeriode(15.0, dato3Start.until(dato3Slutt, ChronoUnit.DAYS).toInt() + 1, erUendelig = true),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp)
+
+ val tidslinjeMåned =
+ tidslinje.konverterTilMåned { _, vindu ->
+ Verdi(vindu[0].last().periodeVerdi.verdi!!)
+ }
+
+ val correct: List = listOf(5, 7, 15)
+
+ Assertions.assertEquals(correct, tidslinjeMåned.innhold.map { it.periodeVerdi.verdi!!.toInt() }.toList())
+ assertTrue(tidslinjeMåned.innhold.last().erUendelig)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/MapOgStripTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/MapOgStripTest.kt
new file mode 100644
index 00000000..f3f088db
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/MapOgStripTest.kt
@@ -0,0 +1,108 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Null
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.Verdi
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class MapOgStripTest {
+ private var lst1 = emptyList>()
+ private var t1: Tidslinje = Tidslinje(LocalDate.now(), emptyList())
+
+ private fun init(lst1: List>) {
+ this.lst1 = lst1
+ this.t1 = Tidslinje(LocalDate.now(), lst1)
+ }
+
+ @Test
+ fun `kan mappe verdiene i en tidslinje til verdier av en annen type`() {
+ init(
+ listOf(
+ TidslinjePeriode(5, 1, false),
+ TidslinjePeriode(3, 1, false),
+ TidslinjePeriode(2, 1, false),
+ TidslinjePeriode(1, 1, false),
+ ),
+ )
+
+ val tidslinje =
+ t1.map {
+ if (it.verdi!! > 1) {
+ Verdi(true)
+ } else {
+ Verdi(false)
+ }
+ }
+
+ val korrekt = listOf(true, false)
+
+ Assertions.assertEquals(korrekt, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ }
+
+ @Test
+ fun `kan mappe udefinert og null til ulike verdier`() {
+ init(
+ listOf(
+ TidslinjePeriode(5, 1, false),
+ TidslinjePeriode(null, 1, false),
+ TidslinjePeriode(2, 1, false),
+ TidslinjePeriode(Udefinert(), 1, false),
+ ),
+ )
+
+ val tidslinje =
+ t1.map {
+ when (it) {
+ is Null -> Verdi(1)
+ is Udefinert -> Verdi(2)
+ else -> Verdi(3)
+ }
+ }
+
+ val korrekt = listOf(3, 1, 3, 2)
+
+ Assertions.assertEquals(korrekt, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ }
+
+ @Test
+ fun `kan strippe vekk udefinert og null fra begynnelsen og slutten av en tidslinje`() {
+ init(
+ listOf(
+ TidslinjePeriode(null, 1, false),
+ TidslinjePeriode(5, 1, false),
+ TidslinjePeriode(2, 1, false),
+ TidslinjePeriode(Udefinert(), 1, false),
+ ),
+ )
+
+ val tidslinje = t1.trim(Udefinert(), Null())
+
+ val korrekt = listOf(5, 2)
+
+ Assertions.assertEquals(korrekt, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(LocalDate.now().plusDays(1), tidslinje.startsTidspunkt)
+ }
+
+ @Test
+ fun `kan strippe vekk TidslinjePerioderverider bestående av heltall`() {
+ init(
+ listOf(
+ TidslinjePeriode(1, 1, false),
+ TidslinjePeriode(5, 1, false),
+ TidslinjePeriode(2, 1, false),
+ TidslinjePeriode(1, 1, false),
+ ),
+ )
+
+ val tidslinje = t1.trim(Verdi(1))
+
+ val korrekt = listOf(5, 2)
+
+ Assertions.assertEquals(korrekt, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(LocalDate.now().plusDays(1), tidslinje.startsTidspunkt)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/OmregningTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/OmregningTest.kt
new file mode 100644
index 00000000..7d6b1b62
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/OmregningTest.kt
@@ -0,0 +1,142 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.TidsEnhet
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
+
+class OmregningTest {
+ @Test
+ fun `kan omgjøre fra uke til dag`() {
+ val dato1Start = LocalDate.of(2022, 7, 4)
+ val dato1Slutt = LocalDate.of(2022, 7, 10)
+
+ val dato2Start = LocalDate.of(2022, 7, 11)
+ val dato2Slutt = LocalDate.of(2022, 7, 17)
+
+ val dato3Start = LocalDate.of(2022, 7, 18)
+ val dato3Slutt = LocalDate.of(2022, 7, 24)
+
+ val dato4Start = LocalDate.of(2022, 7, 25)
+ val dato4Slutt = LocalDate.of(2022, 7, 31)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.WEEKS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.WEEKS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.WEEKS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.WEEKS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp, tidsEnhet = TidsEnhet.UKE)
+
+ val correct = listOf(1, 2, 3, 4)
+
+ val tidslinjeDag = tidslinje.konverterTilDag()
+
+ Assertions.assertEquals(correct, tidslinjeDag.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato1Start, tidslinjeDag.startsTidspunkt)
+
+ Assertions.assertEquals(dato4Slutt, tidslinjeDag.kalkulerSluttTidspunkt())
+ }
+
+ @Test
+ fun `kan omgjøre fra måned til dag`() {
+ val dato1Start = LocalDate.of(2022, 7, 1)
+ val dato1Slutt = LocalDate.of(2022, 7, 31)
+
+ val dato2Start = LocalDate.of(2022, 8, 1)
+ val dato2Slutt = LocalDate.of(2022, 8, 31)
+
+ val dato3Start = LocalDate.of(2022, 9, 1)
+ val dato3Slutt = LocalDate.of(2022, 9, 30)
+
+ val dato4Start = LocalDate.of(2022, 10, 1)
+ val dato4Slutt = LocalDate.of(2022, 10, 31)
+
+ val tmp =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.MONTHS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.MONTHS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.MONTHS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.MONTHS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tmp, tidsEnhet = TidsEnhet.MÅNED)
+
+ val correct = listOf(1, 2, 3, 4)
+
+ val tidslinjeDag = tidslinje.konverterTilDag()
+
+ Assertions.assertEquals(correct, tidslinjeDag.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato1Start, tidslinjeDag.startsTidspunkt)
+
+ Assertions.assertEquals(dato4Slutt, tidslinjeDag.kalkulerSluttTidspunkt())
+ }
+
+ @Test
+ fun `kan omgjøre fra år til dag`() {
+ val dato1Start = LocalDate.of(2022, 1, 1)
+ val dato1Slutt = LocalDate.of(2023, 12, 31)
+
+ val dato2Start = LocalDate.of(2024, 1, 1)
+ val dato2Slutt = LocalDate.of(2024, 12, 31)
+
+ val dato3Start = LocalDate.of(2025, 1, 1)
+ val dato3Slutt = LocalDate.of(2025, 12, 31)
+
+ val dato4Start = LocalDate.of(2026, 1, 1)
+ val dato4Slutt = LocalDate.of(2026, 12, 31)
+
+ val tidslinjePerioder =
+ listOf(
+ TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.YEARS).toInt() + 1),
+ TidslinjePeriode(2, dato2Start.until(dato2Slutt, ChronoUnit.YEARS).toInt() + 1),
+ TidslinjePeriode(3, dato3Start.until(dato3Slutt, ChronoUnit.YEARS).toInt() + 1),
+ TidslinjePeriode(4, dato4Start.until(dato4Slutt, ChronoUnit.YEARS).toInt() + 1),
+ )
+
+ val tidslinje = Tidslinje(dato1Start, tidslinjePerioder, tidsEnhet = TidsEnhet.ÅR)
+
+ val forventedeVerdier = listOf(1, 2, 3, 4)
+
+ val tidslinjeDag = tidslinje.konverterTilDag()
+
+ Assertions.assertEquals(forventedeVerdier, tidslinjeDag.innhold.map { it.periodeVerdi.verdi }.toList())
+
+ Assertions.assertEquals(dato1Start.withDayOfYear(1), tidslinjeDag.startsTidspunkt)
+
+ Assertions.assertEquals(dato4Slutt, tidslinjeDag.kalkulerSluttTidspunkt())
+ }
+
+ @Test
+ fun `Riktig start- og slutttidspunkt`() {
+ val dato1Start = LocalDate.of(2022, 1, 1)
+ val dato1Slutt = LocalDate.of(2023, 12, 31)
+
+ val tmpÅR = listOf(TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.YEARS).toInt() + 1))
+ val tmpMåned = listOf(TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.MONTHS).toInt() + 1))
+ val tmpUke = listOf(TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.WEEKS).toInt() + 1))
+ val tmpDag = listOf(TidslinjePeriode(1, dato1Start.until(dato1Slutt, ChronoUnit.DAYS).toInt() + 1))
+
+ val tidslinjeÅR = Tidslinje(dato1Start, tmpÅR, tidsEnhet = TidsEnhet.ÅR)
+ val tidslinjeMåned = Tidslinje(dato1Start, tmpMåned, tidsEnhet = TidsEnhet.MÅNED)
+ val tidslinjeUke = Tidslinje(dato1Start, tmpUke, tidsEnhet = TidsEnhet.UKE)
+ val tidslinjeDag = Tidslinje(dato1Start, tmpDag, tidsEnhet = TidsEnhet.DAG)
+
+ Assertions.assertEquals(dato1Start, tidslinjeÅR.startsTidspunkt)
+ Assertions.assertEquals(dato1Start, tidslinjeMåned.startsTidspunkt)
+ Assertions.assertEquals(dato1Start, tidslinjeDag.startsTidspunkt)
+ Assertions.assertEquals(LocalDate.of(2021, 12, 27), tidslinjeUke.startsTidspunkt)
+
+ Assertions.assertEquals(dato1Slutt, tidslinjeÅR.kalkulerSluttTidspunkt())
+ Assertions.assertEquals(dato1Slutt, tidslinjeMåned.kalkulerSluttTidspunkt())
+ Assertions.assertEquals(dato1Slutt, tidslinjeUke.kalkulerSluttTidspunkt())
+ Assertions.assertEquals(dato1Slutt, tidslinjeDag.kalkulerSluttTidspunkt())
+ }
+}
diff --git "a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/Sl\303\245SammenLikeTest.kt" "b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/Sl\303\245SammenLikeTest.kt"
new file mode 100644
index 00000000..8c30a959
--- /dev/null
+++ "b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/Sl\303\245SammenLikeTest.kt"
@@ -0,0 +1,69 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Periode
+import no.nav.familie.tidslinje.filtrerIkkeNull
+import no.nav.familie.tidslinje.tilTidslinje
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class SlåSammenLikeTest {
+ private val førsteJanuar = LocalDate.of(2022, 1, 1)
+ private val sisteDagIJanuar = LocalDate.of(2022, 1, 31)
+ private val sisteDagIMars = LocalDate.of(2022, 3, 31)
+ private val førsteApril = LocalDate.of(2022, 4, 1)
+ private val sisteDagIApril = LocalDate.of(2022, 4, 30)
+
+ @Test
+ fun `slåSammenLike - Skal slå sammen like perioder som ligger inntil hverandre`() {
+ val tidslinje =
+ listOf(Periode("a", førsteJanuar, sisteDagIMars), Periode("a", førsteApril, sisteDagIApril)).tilTidslinje()
+
+ val tidslinjeSlåttSammen = tidslinje.slåSammenLikePerioder()
+ val perioder = tidslinjeSlåttSammen.tilPerioder()
+
+ Assertions.assertEquals(1, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIApril, perioder[0].tom)
+ Assertions.assertEquals("a", perioder[0].verdi)
+ }
+
+ @Test
+ fun `slåSammenLike - Skal ikke slå sammen like perioder som ikke ligger inntil hverandre`() {
+ val tidslinje =
+ listOf(Periode("a", førsteJanuar, sisteDagIJanuar), Periode("a", førsteApril, sisteDagIApril)).tilTidslinje()
+
+ val tidslinjeSlåttSammen = tidslinje.slåSammenLikePerioder()
+ val perioder = tidslinjeSlåttSammen.tilPerioder().filtrerIkkeNull()
+
+ Assertions.assertEquals(2, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIJanuar, perioder[0].tom)
+ Assertions.assertEquals("a", perioder[0].verdi)
+
+ Assertions.assertEquals(førsteApril, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIApril, perioder[1].tom)
+ Assertions.assertEquals("a", perioder[1].verdi)
+ }
+
+ @Test
+ fun `slåSammenLike - Skal ikke slå sammen ulike perioder som ligger inntil hverandre`() {
+ val tidslinje =
+ listOf(Periode("a", førsteJanuar, sisteDagIMars), Periode("b", førsteApril, sisteDagIApril)).tilTidslinje()
+
+ val tidslinjeSlåttSammen = tidslinje.slåSammenLikePerioder()
+ val perioder = tidslinjeSlåttSammen.tilPerioder()
+
+ Assertions.assertEquals(2, perioder.size)
+
+ Assertions.assertEquals(førsteJanuar, perioder[0].fom)
+ Assertions.assertEquals(sisteDagIMars, perioder[0].tom)
+ Assertions.assertEquals("a", perioder[0].verdi)
+
+ Assertions.assertEquals(førsteApril, perioder[1].fom)
+ Assertions.assertEquals(sisteDagIApril, perioder[1].tom)
+ Assertions.assertEquals("b", perioder[1].verdi)
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/SnittOgKlippTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/SnittOgKlippTest.kt
new file mode 100644
index 00000000..459e9fb7
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/SnittOgKlippTest.kt
@@ -0,0 +1,109 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.TidsEnhet
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import no.nav.familie.tidslinje.Udefinert
+import no.nav.familie.tidslinje.Verdi
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class SnittOgKlippTest {
+ @Test
+ fun `kan ta snittet av to tidslinjer med ulik lengde`() {
+ val tidslinje1 =
+ Tidslinje(
+ startsTidspunkt = LocalDate.of(2022, 2, 1),
+ perioder =
+ listOf(
+ TidslinjePeriode(periodeVerdi = 1, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 2, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 15, lengde = 2, erUendelig = false),
+ ),
+ tidsEnhet = TidsEnhet.MÅNED,
+ )
+
+ val tidslinje2 =
+ Tidslinje(
+ startsTidspunkt = LocalDate.of(2022, 2, 1),
+ perioder =
+ listOf(
+ TidslinjePeriode(periodeVerdi = 1, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 2, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 15, lengde = 11, erUendelig = false),
+ ),
+ tidsEnhet = TidsEnhet.MÅNED,
+ )
+
+ val resultat =
+ tidslinje1.biFunksjonSnitt(tidslinje2) { el1, el2 ->
+ if (el1 is Udefinert || el2 is Udefinert) {
+ Udefinert()
+ } else {
+ Verdi(el1.verdi!! + el2.verdi!!)
+ }
+ }
+
+ val fasit = mutableListOf(2, 4, 30)
+
+ Assertions.assertEquals(
+ fasit,
+ resultat.innhold.map { it.periodeVerdi.verdi }.toList(),
+ "Kunne ikke addere to tidslinjer med ulik slutt på månedsnivå",
+ )
+ Assertions.assertEquals(resultat.tidsEnhet, TidsEnhet.MÅNED)
+
+ val endDate = LocalDate.of(2022, 2, 1).plusMonths(3)
+ Assertions.assertEquals(endDate.withDayOfMonth(endDate.lengthOfMonth()), resultat.kalkulerSluttTidspunkt())
+ }
+
+ @Test
+ fun `kan klippe en tidslinje slik at dem får riktig start og sluttdato`() {
+ var tidslinje =
+ Tidslinje(
+ startsTidspunkt = LocalDate.of(2022, 2, 1),
+ perioder =
+ listOf(
+ TidslinjePeriode(periodeVerdi = 1, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 2, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 15, lengde = 11, erUendelig = false),
+ ),
+ tidsEnhet = TidsEnhet.MÅNED,
+ )
+
+ var startDato = LocalDate.of(2022, 3, 1)
+ var sluttDato = LocalDate.of(2022, 4, 30)
+
+ var fasit = mutableListOf(2, 15)
+
+ tidslinje = tidslinje.klipp(startDato, sluttDato)
+
+ Assertions.assertEquals(fasit, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(startDato, tidslinje.startsTidspunkt)
+ Assertions.assertEquals(sluttDato, tidslinje.kalkulerSluttTidspunkt())
+
+ tidslinje =
+ Tidslinje(
+ startsTidspunkt = LocalDate.of(2022, 2, 1),
+ perioder =
+ listOf(
+ TidslinjePeriode(periodeVerdi = 1, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 2, lengde = 1, erUendelig = false),
+ TidslinjePeriode(periodeVerdi = 15, lengde = 11, erUendelig = false),
+ ),
+ tidsEnhet = TidsEnhet.MÅNED,
+ )
+
+ startDato = LocalDate.of(2022, 2, 3)
+ sluttDato = LocalDate.of(2022, 2, 6)
+
+ fasit = mutableListOf(1)
+
+ tidslinje = tidslinje.konverterTilDag().klipp(startDato, sluttDato)
+
+ Assertions.assertEquals(fasit, tidslinje.innhold.map { it.periodeVerdi.verdi }.toList())
+ Assertions.assertEquals(startDato, tidslinje.startsTidspunkt)
+ Assertions.assertEquals(sluttDato, tidslinje.kalkulerSluttTidspunkt())
+ }
+}
diff --git a/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeEnkelTest.kt b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeEnkelTest.kt
new file mode 100644
index 00000000..152ea28a
--- /dev/null
+++ b/tidslinje/src/test/kotlin/no/nav/familie/tidslinje/utvidelser/TidslinjeEnkelTest.kt
@@ -0,0 +1,180 @@
+package no.nav.familie.tidslinje.utvidelser
+
+import no.nav.familie.tidslinje.Tidslinje
+import no.nav.familie.tidslinje.TidslinjePeriode
+import no.nav.familie.tidslinje.Verdi
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+
+class TidslinjeEnkelTest {
+ private var lst1 = emptyList>()
+ private var lst2 = emptyList>()
+
+ private var t1: Tidslinje = Tidslinje(LocalDate.now(), emptyList())
+ private var t2: Tidslinje