Skip to content

Commit

Permalink
NAV-23269: Legger til tidslinje fra KS sak i familie-felles (#845)
Browse files Browse the repository at this point in the history
* NAV-23269: Legger til tidslinje for BA og KS sak

* NAV-23269: Legger til tidslinje for BA og KS sak

* NAV-23269: Kjørte ktlint
  • Loading branch information
thoalm authored Nov 21, 2024
1 parent be51eee commit 7d2949d
Show file tree
Hide file tree
Showing 29 changed files with 4,020 additions and 0 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<module>valutakurs-klient</module>
<module>unleash</module>
<module>metrikker</module>
<module>tidslinje</module>
</modules>

<dependencyManagement>
Expand Down
58 changes: 58 additions & 0 deletions tidslinje/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>no.nav.familie.felles</groupId>
<artifactId>felles</artifactId>
<version>${revision}${sha1}${changelist}</version>
</parent>

<artifactId>tidslinje</artifactId>
<version>${revision}${sha1}${changelist}</version>
<name>Felles - Tidslinje</name>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>

<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
31 changes: 31 additions & 0 deletions tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Periode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@file:Suppress("UNCHECKED_CAST")

package no.nav.familie.tidslinje

import java.time.LocalDate

data class Periode<T>(
// 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 <T> List<Periode<T>>.tilTidslinje(): Tidslinje<T> =
this
.map { it.tilTidslinjePeriodeMedDato() }
.sortedBy { it.fom }
.tilTidslinje()

fun <T> List<Periode<T>>.filtrerIkkeNull(): List<Periode<T & Any>> =
this.mapNotNull { periode -> periode.verdi?.let { periode as Periode<T & Any> } }

fun <T> List<Periode<T>>.verdier(): List<T> = this.map { it.verdi }

data class IkkeNullbarPeriode<T>(
val verdi: T,
val fom: LocalDate,
val tom: LocalDate,
)
207 changes: 207 additions & 0 deletions tidslinje/src/main/kotlin/no/nav/familie/tidslinje/Tidslinje.kt
Original file line number Diff line number Diff line change
@@ -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<T>(
var startsTidspunkt: LocalDate,
perioder: List<TidslinjePeriode<T>>,
var tidsEnhet: TidsEnhet = TidsEnhet.DAG,
) {
var innhold: List<TidslinjePeriode<T>> = emptyList()
set(verdi) {
field = this.lagInnholdBasertPåPeriodelengder(verdi)
}
val foreldre: MutableList<Tidslinje<Any>> = mutableListOf()
var tittel = ""

init {
this.innhold = perioder
startsTidspunkt =
when (tidsEnhet) {
TidsEnhetR -> 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) {
TidsEnhetR -> 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) {
TidsEnhetR -> 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<TidslinjePeriode<T>>): List<TidslinjePeriode<T>> {
val arr = mutableListOf<TidslinjePeriode<T>>()

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 <T> tomTidslinje(
startsTidspunkt: LocalDate? = null,
tidsEnhet: TidsEnhet = TidsEnhet.DAG,
): Tidslinje<T> = Tidslinje(startsTidspunkt = startsTidspunkt ?: PRAKTISK_TIDLIGSTE_DAG, emptyList(), tidsEnhet)

fun <K, V, H, R> Map<K, Tidslinje<V>>.leftJoin(
yreTidslinjer: Map<K, Tidslinje<H>>,
kombinator: (V?, H?) -> R?,
): Map<K, Tidslinje<R>> {
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 <K, V, H, R> Map<K, Tidslinje<V>>.outerJoin(
yreTidslinjer: Map<K, Tidslinje<H>>,
kombinator: (V?, H?) -> R?,
): Map<K, Tidslinje<R>> {
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 <K, A, B, C, R> Map<K, Tidslinje<A>>.outerJoin(
tidslinjer2: Map<K, Tidslinje<B>>,
tidslinjer3: Map<K, Tidslinje<C>>,
kombinator: (A?, B?, C?) -> R?,
): Map<K, Tidslinje<R>> {
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 <T> Tidslinje<T>.beskjærEtter(tidslinje: Tidslinje<*>): Tidslinje<T> =
this.klipp(tidslinje.startsTidspunkt, tidslinje.kalkulerSluttTidspunkt())

fun <T> Tidslinje<T>.inneholder(verdi: T): Boolean = this.tilPerioder().any { it.verdi == verdi }

fun <T, R> Tidslinje<T>.mapVerdi(mapper: (T?) -> R): Tidslinje<R> =
this.map { periodeVerdi ->
when (periodeVerdi) {
is Verdi,
is Null,
-> mapper(periodeVerdi.verdi)?.let { Verdi(it) } ?: Null()

is Udefinert -> Udefinert()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package no.nav.familie.tidslinje

const val INF = 1_000_000_000

sealed class PeriodeVerdi<T>(
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<T>(
override val verdi: T & Any,
) : PeriodeVerdi<T>(verdi)

class Udefinert<T> : PeriodeVerdi<T>(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<T> : PeriodeVerdi<T>(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<T>(
val periodeVerdi: PeriodeVerdi<T>,
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> T?.tilPeriodeVerdi(): PeriodeVerdi<T> = this?.let { Verdi(it) } ?: Null()
Loading

0 comments on commit 7d2949d

Please sign in to comment.