mon-entreprise/source/engine/period.ts

425 lines
9.8 KiB
TypeScript

import {
convertToDate,
getDifferenceInDays,
getDifferenceInMonths,
getDifferenceInYears,
getRelativeDate,
getYear
} from 'Engine/date'
import { Unit } from './units'
export type Period<T> = {
start: T | null
end: T | null
}
export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
const startWords = [
'depuis',
'depuis le',
'depuis la',
'à partir de',
'à partir du',
'du'
]
const endWords = [
"jusqu'à",
"jusqu'au",
"jusqu'à la",
'avant',
'avant le',
'avant la',
'au'
]
const intervalWords = ['le', 'en']
if (!startWords.concat(endWords, intervalWords).includes(word)) {
throw new SyntaxError(
`Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join(
', '
)}`
)
}
if (word === 'le') {
return {
start: date,
end: date
}
}
if (word === 'en') {
return { start: null, end: null }
}
if (startWords.includes(word)) {
return {
start: date,
end: null
}
}
if (endWords.includes(word)) {
return {
start: null,
end: date
}
}
throw new Error('Non implémenté')
}
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
export type Evaluation<T> = T | false | null
export type EvaluatedNode<T> = {
nodeValue: Evaluation<T>
temporalValue?: Temporal<Evaluation<T>>
}
export type TemporalNode<T> = Temporal<{ nodeValue: Evaluation<T> }>
export type Temporal<T> = Array<Period<string> & { value: T }>
export function narrowTemporalValue<T>(
period: Period<string>,
temporalValue: Temporal<Evaluation<T>>
): Temporal<Evaluation<T>> {
return liftTemporal2(
(value, filter) => filter && value,
temporalValue,
createTemporalEvaluation(true, period)
)
}
// Returns a temporal value that's true for the given period and false otherwise.
export function createTemporalEvaluation<T>(
value: Evaluation<T>,
period: Period<string> = { start: null, end: null }
): Temporal<Evaluation<T>> {
let temporalValue = [{ ...period, value }]
if (period.start != null) {
temporalValue.unshift({
start: null,
end: getRelativeDate(period.start, -1),
value: false
})
}
if (period.end != null) {
temporalValue.push({
start: getRelativeDate(period.end, 1),
end: null,
value: false
})
}
return temporalValue
}
export function pureTemporal<T>(value: T): Temporal<T> {
return [{ start: null, end: null, value }]
}
export function mapTemporal<T1, T2>(
fn: (value: T1) => T2,
temporalValue: Temporal<T1>
): Temporal<T2> {
return temporalValue.map(({ start, end, value }) => ({
start,
end,
value: fn(value)
}))
}
export function sometime<T1>(
fn: (value: T1) => boolean,
temporalValue: Temporal<T1>
): boolean {
return temporalValue.some(({ start, end, value }) => fn(value))
}
export function liftTemporal2<T1, T2, T3>(
fn: (value1: T1, value2: T2) => T3,
temporalValue1: Temporal<T1>,
temporalValue2: Temporal<T2>
): Temporal<T3> {
return mapTemporal(
([a, b]) => fn(a, b),
zipTemporals(temporalValue1, temporalValue2)
)
}
export function concatTemporals<T, U>(
temporalValues: Array<Temporal<T>>
): Temporal<Array<T>> {
return temporalValues.reduce(
(values, value) => liftTemporal2((a, b) => [...a, b], values, value),
pureTemporal([]) as Temporal<Array<T>>
)
}
export function liftTemporalNode<T>(node: EvaluatedNode<T>): TemporalNode<T> {
const { temporalValue, ...baseNode } = node
if (!temporalValue) {
return pureTemporal(baseNode)
}
return mapTemporal(
nodeValue => ({
...baseNode,
nodeValue
}),
temporalValue
)
}
export function zipTemporals<T1, T2>(
temporalValue1: Temporal<T1>,
temporalValue2: Temporal<T2>,
acc: Temporal<[T1, T2]> = []
): Temporal<[T1, T2]> {
if (!temporalValue1.length && !temporalValue2.length) {
return acc
}
const [value1, ...rest1] = temporalValue1
const [value2, ...rest2] = temporalValue2
console.assert(value1.start === value2.start)
const endDateComparison = compareEndDate(value1.end, value2.end)
// End dates are equals
if (endDateComparison === 0) {
return zipTemporals(rest1, rest2, [
...acc,
{ ...value1, value: [value1.value, value2.value] }
])
}
// Value1 lasts longuer than value1
if (endDateComparison > 0) {
console.assert(value2.end !== null)
return zipTemporals(
[
{ ...value1, start: getRelativeDate(value2.end as string, 1) },
...rest1
],
rest2,
[
...acc,
{
...value2,
value: [value1.value, value2.value]
}
]
)
}
// Value2 lasts longuer than value1
if (endDateComparison < 0) {
console.assert(value1.end !== null)
return zipTemporals(
rest1,
[
{ ...value2, start: getRelativeDate(value1.end as string, 1) },
...rest2
],
[
...acc,
{
...value1,
value: [value1.value, value2.value]
}
]
)
}
throw new EvalError('All case should have been covered')
}
function beginningOfNextYear(date: string): string {
return `01/01/${getYear(date) + 1}`
}
function endsOfPreviousYear(date: string): string {
return `31/12/${getYear(date) - 1}`
}
function splitStartsAt<T>(
fn: (date: string) => string,
temporal: Temporal<T>
): Temporal<T> {
return temporal.reduce((acc, period) => {
const { start, end } = period
const newStart = start === null ? start : fn(start)
if (compareEndDate(newStart, end) !== -1) {
return [...acc, period]
}
console.assert(newStart !== null)
return [
...acc,
{ ...period, end: getRelativeDate(newStart as string, -1) },
{ ...period, start: newStart }
]
}, [] as Temporal<T>)
}
function splitEndsAt<T>(
fn: (date: string) => string,
temporal: Temporal<T>
): Temporal<T> {
return temporal.reduce((acc, period) => {
const { start, end } = period
const newEnd = end === null ? end : fn(end)
if (compareStartDate(start, newEnd) !== -1) {
return [...acc, period]
}
console.assert(newEnd !== null)
return [
...acc,
{ ...period, end: newEnd },
{ ...period, start: getRelativeDate(newEnd as string, 1) }
]
}, [] as Temporal<T>)
}
export function groupByYear<T>(temporalValue: Temporal<T>): Array<Temporal<T>> {
return (
// First step: split period by year if needed
splitEndsAt(
endsOfPreviousYear,
splitStartsAt(beginningOfNextYear, temporalValue)
)
// Second step: group period by year
.reduce((acc, period) => {
const [currentTemporal, ...otherTemporal] = acc
if (currentTemporal === undefined) {
return [[period]]
}
const firstPeriod = currentTemporal[0]
console.assert(
firstPeriod !== undefined &&
firstPeriod.end !== null &&
period.start !== null,
'invariant non verifié'
)
if (
(firstPeriod.end as string).slice(-4) !==
(period.start as string).slice(-4)
) {
return [[period], ...acc]
}
return [[...currentTemporal, period], ...otherTemporal]
}, [] as Array<Temporal<T>>)
.reverse()
)
}
function simplify<T>(temporalValue: Temporal<T>): Temporal<T> {
return temporalValue
}
function compareStartDate(
dateA: string | null,
dateB: string | null
): -1 | 0 | 1 {
if (dateA == dateB) {
return 0
}
if (dateA == null) {
return -1
}
if (dateB == null) {
return 1
}
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
}
function compareEndDate(
dateA: string | null,
dateB: string | null
): -1 | 0 | 1 {
if (dateA == dateB) {
return 0
}
if (dateA == null) {
return 1
}
if (dateB == null) {
return -1
}
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
}
export function temporalAverage(
temporalValue: Temporal<Evaluation<number>>,
unit?: Unit
): Evaluation<number> {
temporalValue = temporalValue.filter(({ value }) => value !== false)
const first = temporalValue[0]
const last = temporalValue[temporalValue.length - 1]
if (!temporalValue.length) {
return false
}
if (temporalValue.length === 1) {
return temporalValue[0].value
}
// La variable est définie sur un interval infini
if (first.start == null || last.end == null) {
if (first.start != null) {
return last.value
}
if (last.end != null) {
return first.value
}
return (first.value + last.value) / 2
}
if (temporalValue.some(({ value }) => value == null)) {
return null
}
let totalWeight = 0
const weights = temporalValue.map(({ start, end, value }) => {
let weight = 0
if (unit?.denominators.includes('mois')) {
weight = getDifferenceInMonths(start, end)
} else if (unit?.denominators.includes('année')) {
weight = getDifferenceInYears(start, end)
} else {
weight = getDifferenceInDays(start, end)
}
totalWeight += weight
return (value as number) * weight
})
return weights.reduce(
(average, weightedValue) => average + weightedValue / totalWeight,
0
)
}
export function temporalCumul(
temporalValue: Temporal<Evaluation<number>>,
unit: Unit
): Evaluation<number> {
temporalValue = temporalValue.filter(({ value }) => value !== false)
const first = temporalValue[0]
const last = temporalValue[temporalValue.length - 1]
if (!temporalValue.length) {
return false
}
// La variable est définie sur un interval infini
if (first.start == null || last.end == null) {
if (first.start != null) {
return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
}
if (last.end != null) {
return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
}
return null
}
if (temporalValue.some(({ value }) => value == null)) {
return null
}
return temporalValue.reduce((acc, { start, end, value }) => {
let weight = 1
if (unit?.denominators.includes('mois')) {
weight = getDifferenceInMonths(start, end)
} else if (unit?.denominators.includes('année')) {
weight = getDifferenceInYears(start, end)
} else if (unit?.denominators.includes('jour')) {
weight = getDifferenceInDays(start, end)
}
return value * weight + acc
}, 0)
}