feat: refactor + ajoute Nb familleMuses en demeure

wip-related-pages
sebastien.arod@gmail.com 2024-06-03 21:56:35 +02:00
parent 3de3aa2662
commit 54fcebebf0
17 changed files with 406 additions and 275 deletions

101
src/data/Famille.ts Normal file
View File

@ -0,0 +1,101 @@
import { differenceInDays } from "date-fns";
import { Period } from "../period/Period";
import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping";
import { isPeriodContaining } from "../period/isPeriodContaining";
export type Famille = {
notionId: string;
Titre: string;
Statut: StatutFamille;
Integration: Date | null;
Sortie: Date | null;
Evenements: EvenementFamille[];
};
export type EvenementFamille = {
notionId: string;
notionIdFamille: string;
Évènement: string;
Date: Date | null;
Type: TypeEvenement;
"Enfants concernés": string;
};
export type TypeEvenement =
| "Mise en demeure de scolarisation"
| "Composition pénale refusée"
| "Composition pénale acceptée";
export type StatutFamille =
| "Résistant.e"
| "Ex résistant·e·s"
| "À préciser"
| "Se questionne"
| "Désobéissence décidée"
| "Rédaction Déclaration"
| "Déclaration Validée - Attente éléments"
| "Abdandon"
| "Incompatible";
export function periodOfResistance(
family: Famille,
atDate: Date = new Date(Date.now())
): Period | null {
if (family.Statut !== "Résistant.e" && family.Statut !== "Ex résistant·e·s") {
return null;
}
if (!family.Integration || family.Integration.getTime() > atDate.getTime()) {
return null;
}
return {
start: family.Integration,
end:
family.Sortie !== null && family.Sortie.getTime() < atDate.getTime()
? family.Sortie
: atDate,
};
}
export function isResistant(
family: Famille,
date: Date = new Date(Date.now())
): boolean {
const por = periodOfResistance(family, date);
return por !== null && isPeriodContaining(por, date);
}
export function isExResistant(
family: Famille,
date: Date = new Date(Date.now())
): boolean {
const por = periodOfResistance(family, date);
return por !== null && por.end.getTime() < date.getTime();
}
export function isResistantOverPeriod(
family: Famille,
period: Period
): boolean {
const familyPeriodResistant: Period | null = periodOfResistance(family);
return (
familyPeriodResistant !== null &&
arePeriodsOverlaping(familyPeriodResistant, period)
);
}
/**
*
* @param family
* @param atDate
* @returns the duration of resistance in days or null if family was not yet in resistances at this date
*/
export function dureeResistanceInDays(
family: Famille,
atDate: Date = new Date(Date.now())
): number | null {
const period = periodOfResistance(family, atDate);
if (period == null) {
return null;
}
return differenceInDays(period.end, period.start);
}

View File

@ -1,87 +0,0 @@
import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping";
import { Period } from "../period/Period";
export type Family = {
notionId: string;
Titre: string;
Statut: StatutFamille;
Integration: Date | null;
Sortie: Date | null;
Evenements: FamilyEvent[];
};
export type FamilyEvent = {
notionId: string;
notionIdFamille: string;
Évènement: string;
Date: Date | null;
Type: string;
"Enfants concernés": string;
};
export type StatutFamille =
| "Résistant.e"
| "Ex résistant·e·s"
| "À préciser"
| "Se questionne"
| "Désobéissence décidée"
| "Rédaction Déclaration"
| "Déclaration Validée - Attente éléments"
| "Abdandon"
| "Incompatible";
export function isResistant(family: Family): boolean {
return (
family.Statut === "Résistant.e" &&
family.Integration !== null &&
family.Sortie === null
);
}
export function isExResistant(family: Family): boolean {
return (
family.Statut === "Ex résistant·e·s" &&
family.Integration !== null &&
family.Sortie !== null
);
}
export function isResistantAtDate(family: Family, date: Date): boolean {
if (isResistant(family) && family.Integration!.getTime() <= date.getTime()) {
return true;
}
if (
isExResistant(family) &&
family.Integration!.getTime() <= date.getTime() &&
family.Sortie!.getTime() > date.getTime()
) {
return true;
}
return false;
}
export function isResistantOverPeriod(family: Family, period: Period): boolean {
const familyPeriodResistant: Period | null = periodOfResistance(family);
return (
familyPeriodResistant !== null &&
arePeriodsOverlaping(familyPeriodResistant, period)
);
}
export function periodOfResistance(family: Family): Period | null {
if (isResistant(family)) {
const periodResistant: Period = {
start: family.Integration!,
end: new Date(Date.now()),
};
return periodResistant;
}
if (isExResistant(family)) {
const periodResistant: Period = {
start: family.Integration!,
end: family.Sortie!,
};
return periodResistant;
}
return null;
}

View File

@ -1,6 +1,6 @@
import { Family } from "./Family";
import { Famille } from "./Famille";
export function checkDataConsistency(families: Family[]): ConsistencyIssue[] {
export function checkDataConsistency(families: Famille[]): ConsistencyIssue[] {
return families.flatMap((family) => {
return checkFamilyDataConsistency(family);
});
@ -10,7 +10,7 @@ export type ConsistencyIssue = {
issueType: string;
familyId: string;
};
function checkFamilyDataConsistency(family: Family) {
function checkFamilyDataConsistency(family: Famille) {
const consistencyIssues: ConsistencyIssue[] = [];
if (family.Statut === "Résistant.e") {

View File

@ -30,8 +30,6 @@ import { computeELStats } from "./statistiques/computeELStats";
console.log("Building statistics...");
const resistantCountStats = computeELStats(families);
console.log(resistantCountStats);
console.log("Publishing statistics...");
publishStatisticsToNotion(resistantCountStats, notionClient);
})();

View File

@ -1,6 +1,11 @@
import { Client, isFullPage } from "@notionhq/client";
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
import { Family, FamilyEvent, StatutFamille } from "../../data/Family";
import {
EvenementFamille,
Famille,
StatutFamille,
TypeEvenement,
} from "../../data/Famille";
import { datePropertyToDate } from "../utils/properties/datePropertyToDate";
import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId";
import { selectPropertyToText } from "../utils/properties/selectPropertyToText";
@ -11,7 +16,7 @@ import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPla
export async function fetchFamiliesWithEventsFromNotion(
notionClient: Client
): Promise<Family[]> {
): Promise<Famille[]> {
const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da";
const familEventsDbId: string = "c4d434b4603c4481a4d445618ecdf999";
@ -30,7 +35,7 @@ export async function fetchFamiliesWithEventsFromNotion(
return buildFamilyEvent(pageObjectResponse);
});
const families: Family[] = await Promise.all(
const families: Famille[] = await Promise.all(
familyPages.map((pageObjectResponse) => {
return buildFamily(pageObjectResponse, familyEvents);
})
@ -38,13 +43,13 @@ export async function fetchFamiliesWithEventsFromNotion(
return families;
}
function buildFamilyEvent(page: PageObjectResponse): FamilyEvent {
function buildFamilyEvent(page: PageObjectResponse): EvenementFamille {
const pageProperties = page.properties;
const familyEvent: FamilyEvent = {
const familyEvent: EvenementFamille = {
notionId: page.id,
Évènement: titlePropertyToText(pageProperties, "Évènement"),
Type: selectPropertyToText(pageProperties, "Type")!,
Type: selectPropertyToText(pageProperties, "Type")! as TypeEvenement,
"Enfants concernés": richTextPropertyToPlainText(
pageProperties,
"Enfants concernés"
@ -57,11 +62,11 @@ function buildFamilyEvent(page: PageObjectResponse): FamilyEvent {
function buildFamily(
page: PageObjectResponse,
familyEvents: FamilyEvent[]
): Family {
familyEvents: EvenementFamille[]
): Famille {
const pageProperties = page.properties;
const family: Family = {
const family: Famille = {
notionId: page.id,
Titre: titlePropertyToText(pageProperties, ""),
Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille,

View File

@ -0,0 +1,140 @@
import { Client, isFullPage } from "@notionhq/client";
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
import { ELPeriodStats, ValueWithEvol } from "../../statistiques/ELStats";
import { titlePropertyToText } from "../utils/properties/titlePropertyToText";
import { queryAllDbResults } from "../utils/queryAllDbResults";
import { removeBlocks } from "../utils/removeBlocks";
import { CreatePageProperties } from "../utils/types/CreatePageProperties";
import {
statNameDureeResistanceMediane,
statNameDureeResistanceMoyenne,
statsNameNbFamillesMisesEnDemeure,
statsNameNbFamillesResistantes,
} from "./statNames";
export async function publishPeriodStats(
notionClient: Client,
periodStatsDbId: string,
stats: ELPeriodStats[]
) {
const periodRows = (
await queryAllDbResults(notionClient, {
database_id: periodStatsDbId,
})
).filter(isFullPage);
const indexedPeriodRows: { [period: string]: PageObjectResponse } =
Object.fromEntries(
periodRows.map((r) => [titlePropertyToText(r.properties, "Période"), r])
);
const indexedPeriodStats = Object.fromEntries(
stats.map((stat) => [stat.periodId, stat])
);
const rowIdsToDelete = Object.entries(indexedPeriodRows)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([, row]) => row.id);
const periodIdsToUpdate = Object.entries(indexedPeriodRows)
.filter(([periodId]) =>
Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([periodId]) => periodId);
const periodIdsToCreate = Object.entries(indexedPeriodStats)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodRows, periodId)
)
.map(([periodId]) => periodId);
// Delete rows to delte
await removeBlocks(notionClient, rowIdsToDelete);
// Create rows to create
for (const periodId of periodIdsToCreate) {
const stat = indexedPeriodStats[periodId];
await notionClient.pages.create({
parent: {
database_id: periodStatsDbId,
},
properties: buildRowPropertiesForUpsert(stat),
});
}
// Update rows
for (const periodId of periodIdsToUpdate) {
const stat = indexedPeriodStats[periodId];
const row = indexedPeriodRows[periodId];
await notionClient.pages.update({
page_id: row.id,
properties: buildRowPropertiesForUpsert(stat),
});
}
}
function buildRowPropertiesForUpsert(
stat: ELPeriodStats
): CreatePageProperties {
return {
Période: {
title: [
{
text: {
content: stat.periodId,
},
},
],
},
[statsNameNbFamillesResistantes]: valueWithEvolProp(
stat.nbFamilleResistantes
),
[statNameDureeResistanceMediane]: valueWithEvolProp(
stat.dureeResistanceMediane
),
[statNameDureeResistanceMoyenne]: valueWithEvolProp(
stat.dureeResistanceMoyenne
),
[statsNameNbFamillesMisesEnDemeure]: valueWithEvolProp(
stat.nbFamillesMisesEnDemeure
),
};
}
function valueWithEvolProp(n: ValueWithEvol) {
const formatted = formatValueWithEvol(n);
return {
rich_text: [
{
text: {
content: formatted,
},
},
],
};
}
function formatValueWithEvol(n: ValueWithEvol): string {
const value = n.value.toLocaleString("fr-FR", {
useGrouping: false,
maximumFractionDigits: 2,
});
if (isNaN(n.evol)) {
return value;
} else {
const evol = n.evol.toLocaleString("fr-FR", {
useGrouping: false,
maximumFractionDigits: 2,
signDisplay: "always",
});
const evolPercent = Math.round(n.evolPercent).toLocaleString("fr-FR", {
useGrouping: false,
signDisplay: "always",
});
return `${value} (${evol} | ${evolPercent}%)`;
}
}

View File

@ -1,14 +1,7 @@
import { Client, isFullPage } from "@notionhq/client";
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
import {
ELPeriodStats,
ELStats,
ValueWithEvol,
} from "../../statistiques/ELStats";
import { titlePropertyToText } from "../utils/properties/titlePropertyToText";
import { queryAllDbResults } from "../utils/queryAllDbResults";
import { removeBlocks } from "../utils/removeBlocks";
import { publishCurrentStats } from "./publishCurrentStats";
import { Client } from "@notionhq/client";
import { ELStats } from "../../statistiques/ELStats";
import { publishPeriodStats } from "./publishPeriodStats";
import { publishStatsActuelles } from "./publishStatsActuelles";
export const statsPageId = "2b91cd90e3694e96bb196d69aeca59b1";
export const currentStatsHeading = "Statistiques actuelles";
@ -19,123 +12,9 @@ export async function publishStatisticsToNotion(
stats: ELStats,
notionClient: Client
) {
await publishCurrentStats(notionClient, stats);
await publishStatsActuelles(notionClient, stats.actuelles);
await publishPeriodStats(notionClient, yearStatsDb, stats.annees);
await publishPeriodStats(notionClient, monthStatsDb, stats.mois);
}
async function publishPeriodStats(
notionClient: Client,
periodStatsDbId: string,
stats: ELPeriodStats[]
) {
const periodRows = (
await queryAllDbResults(notionClient, {
database_id: periodStatsDbId,
})
).filter(isFullPage);
const indexedPeriodRows: { [period: string]: PageObjectResponse } =
Object.fromEntries(
periodRows.map((r) => [titlePropertyToText(r.properties, "Période"), r])
);
const indexedPeriodStats = Object.fromEntries(
stats.map((stat) => [stat.periodId, stat])
);
const rowIdsToDelete = Object.entries(indexedPeriodRows)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([, row]) => row.id);
const periodIdsToUpdate = Object.entries(indexedPeriodRows)
.filter(([periodId]) =>
Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([periodId]) => periodId);
const periodIdsToCreate = Object.entries(indexedPeriodStats)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodRows, periodId)
)
.map(([periodId]) => periodId);
// Delete rows to delte
await removeBlocks(notionClient, rowIdsToDelete);
// Create rows to create
for (const periodId of periodIdsToCreate) {
const stat = indexedPeriodStats[periodId];
await notionClient.pages.create({
parent: {
database_id: periodStatsDbId,
},
properties: buildRowPropertiesForUpsert(stat),
});
}
// Update rows
for (const periodId of periodIdsToUpdate) {
const stat = indexedPeriodStats[periodId];
const row = indexedPeriodRows[periodId];
await notionClient.pages.update({
page_id: row.id,
properties: buildRowPropertiesForUpsert(stat),
});
}
}
function buildRowPropertiesForUpsert(stat: ELPeriodStats) {
return {
Période: {
title: [
{
text: {
content: stat.periodId,
},
},
],
},
"Nb Famille Résistante": valueWithEvolProp(stat.nbFamilleResistantes),
};
}
function valueWithEvolProp(n: ValueWithEvol) {
const formatted = formatValueWithEvol(n);
return {
rich_text: [
{
text: {
content: formatted,
},
},
],
};
}
function formatValueWithEvol(n: ValueWithEvol): string {
const value = n.value.toLocaleString("fr-FR", {
useGrouping: false,
maximumFractionDigits: 2,
});
if (isNaN(n.evol)) {
return value;
} else {
const evol = n.evol.toLocaleString("fr-FR", {
useGrouping: false,
maximumFractionDigits: 2,
signDisplay: "always",
});
const evolPercent = Math.round(n.evolPercent).toLocaleString("fr-FR", {
useGrouping: false,
signDisplay: "always",
});
return `${value} (${evol} | ${evolPercent}%)`;
}
}

View File

@ -1,14 +1,22 @@
import { Client, isFullBlock } from "@notionhq/client";
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
import { ELStats } from "../../statistiques/ELStats";
import { ELStatsActuelles } from "../../statistiques/ELStats";
import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks";
import { removeBlocks } from "../utils/removeBlocks";
import { richTextToPlainText } from "../utils/text/richTextToPlainText";
import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion";
import {
statNameDureeResistanceMediane,
statNameDureeResistanceMoyenne,
statsNameNbFamillesMisesEnDemeure,
statsNameNbFamillesResistantes,
statsNameNbFamillesResistantesOuEx,
statsNamePourcentageFamilleMisesEnDemeure,
} from "./statNames";
export async function publishCurrentStats(
export async function publishStatsActuelles(
notionClient: Client,
stats: ELStats
statsActuelles: ELStatsActuelles
) {
const childrenBlocks = (
await listAllChildrenBlocks(notionClient, {
@ -41,18 +49,29 @@ export async function publishCurrentStats(
block_id: statsPageId,
after: currentStatsHeadingBlock.id,
children: [
currentStatBlock("Nb Famille Résistante", stats.nbFamilleResistantes),
currentStatBlock(
"Nb Famille Résistante ou Ex-Résistante",
stats.nbFamilleResistantesOrEx
statsNameNbFamillesResistantes,
statsActuelles.nbFamilleResistantes
),
currentStatBlock(
"Durée Moyenne Résistance (jours)",
stats.dureeMoyenneResistance
statsNameNbFamillesResistantesOuEx,
statsActuelles.nbFamilleResistantesOrEx
),
currentStatBlock(
"Durée Médiane Résistance (jours)",
stats.dureeMedianeResistance
statNameDureeResistanceMoyenne,
statsActuelles.dureeResistanceMoyenne
),
currentStatBlock(
statNameDureeResistanceMediane,
statsActuelles.dureeResistanceMediane
),
currentStatBlock(
statsNameNbFamillesMisesEnDemeure,
statsActuelles.nbFamillesMiseEnDemeure
),
currentStatBlock(
statsNamePourcentageFamilleMisesEnDemeure,
statsActuelles.pourcentageFamillesMisesEnDemeure
),
],
});

View File

@ -0,0 +1,8 @@
export const statNameDureeResistanceMoyenne = "Durée Résistance Moyenne";
export const statNameDureeResistanceMediane = "Durée Résistance Médiane";
export const statsNameNbFamillesResistantes = "Nb Familles Résistantes";
export const statsNameNbFamillesResistantesOuEx =
"Nb Familles Résistante ou Ex-Résistantes";
export const statsNameNbFamillesMisesEnDemeure = "Nb Familles Mises en Demeure";
export const statsNamePourcentageFamilleMisesEnDemeure =
"Pourcentage de Familles Mises en Demeure";

View File

@ -0,0 +1,3 @@
import { CreatePageParameters } from "@notionhq/client/build/src/api-endpoints";
export type CreatePageProperties = CreatePageParameters["properties"];

View File

@ -1,5 +1,4 @@
export type Period = {
start: Date;
/** Exclusive */
end: Date;
};

View File

@ -25,7 +25,7 @@ describe("isPeriodContaining", () => {
).toBe(true);
});
test("period does not contain end date", () => {
test("period contains end date", () => {
expect(
isPeriodContaining(
{
@ -34,7 +34,7 @@ describe("isPeriodContaining", () => {
},
new Date(Date.UTC(2024, 3, 1))
)
).toBe(false);
).toBe(true);
});
test("period does not contain date before", () => {
expect(

View File

@ -7,7 +7,7 @@ export function isPeriodContaining(
if (dateOrPeriod instanceof Date) {
return (
period.start.getTime() <= dateOrPeriod.getTime() &&
dateOrPeriod.getTime() < period.end.getTime()
dateOrPeriod.getTime() <= period.end.getTime()
);
} else {
return (

View File

@ -1,19 +1,26 @@
export type ELStats = {
actuelles: ELStatsActuelles;
annees: ELPeriodStats[];
mois: ELPeriodStats[];
};
export type ELStatsActuelles = {
nbFamilleResistantes: number;
/** Includes Ancient resistants */
nbFamilleResistantesOrEx: number;
dureeMoyenneResistance: number;
dureeMedianeResistance: number;
annees: ELPeriodStats[];
mois: ELPeriodStats[];
dureeResistanceMoyenne: number;
dureeResistanceMediane: number;
nbFamillesMiseEnDemeure: number;
pourcentageFamillesMisesEnDemeure: number;
};
export type ELPeriodStats = {
periodId: string;
nbFamilleResistantes: ValueWithEvol;
dureeResistanceMoyenne: ValueWithEvol;
dureeResistanceMediane: ValueWithEvol;
nbFamillesMisesEnDemeure: ValueWithEvol;
};
export type ValueWithEvol = {

View File

@ -1,24 +1,59 @@
import { Family, isResistantOverPeriod } from "../data/Family";
import {
Famille,
dureeResistanceInDays,
isResistantOverPeriod,
} from "../data/Famille";
import { IdentifiedPeriod } from "../period/IdentifiedPeriod";
import { isPeriodContaining } from "../period/isPeriodContaining";
import { ELPeriodStats, ValueWithEvol } from "./ELStats";
import { average } from "./math/average";
import { median } from "./math/median";
export function computeELPeriodStats(
familles: Family[],
familles: Famille[],
periods: IdentifiedPeriod[]
): ELPeriodStats[] {
const periodStats: ELPeriodStats[] = [];
let previousELPeriodStats: ELPeriodStats | null = null;
for (const period of periods) {
const resistantsCount = familles.filter((famille) =>
const nbFamilleResistantes = familles.filter((famille) =>
isResistantOverPeriod(famille, period)
).length;
const periodEndOrNow =
period.end.getTime() > Date.now() ? new Date(Date.now()) : period.end;
const dureesResistances = familles
.map((famille) => dureeResistanceInDays(famille, periodEndOrNow))
.filter(notNull);
const dureeResistanceMediane = Math.round(median(dureesResistances));
const dureeResistanceMoyenne = Math.round(average(dureesResistances));
const nbFamillesMiseEnDemeure = familles.filter((famille) =>
famille.Evenements.find(
(e) =>
e.Type === "Mise en demeure de scolarisation" &&
e.Date &&
isPeriodContaining(period, e.Date)
)
).length;
const stats: ELPeriodStats = {
periodId: period.id,
nbFamilleResistantes: valueWithEvol(
resistantsCount,
nbFamilleResistantes,
previousELPeriodStats?.nbFamilleResistantes.value
),
dureeResistanceMediane: valueWithEvol(
dureeResistanceMediane,
previousELPeriodStats?.dureeResistanceMediane.value
),
dureeResistanceMoyenne: valueWithEvol(
dureeResistanceMoyenne,
previousELPeriodStats?.dureeResistanceMoyenne.value
),
nbFamillesMisesEnDemeure: valueWithEvol(
nbFamillesMiseEnDemeure,
previousELPeriodStats?.nbFamillesMisesEnDemeure.value
),
};
periodStats.push(stats);
previousELPeriodStats = stats;
@ -46,3 +81,9 @@ function evol(current: number, previous: number | undefined): number {
if (previous === undefined) return NaN;
return current - previous;
}
// Typescript is not able to infer this inline see
// https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array
function notNull<TValue>(value: TValue | null): value is TValue {
return value !== null;
}

View File

@ -1,28 +1,12 @@
import {
Family,
isExResistant,
isResistant,
periodOfResistance,
} from "../data/Family";
import { daysInPeriod } from "../period/daysInPeriod";
import { Famille } from "../data/Famille";
import { ELStats } from "./ELStats";
import { computeELPeriodStats } from "./computeELPeriodStats";
import { computeStatsActuelles } from "./computeStatsActuelles";
import { generateELMonths } from "./generateELMonths";
import { generateELYears } from "./generateELYears";
import { average } from "./math/average";
import { median } from "./math/median";
export function computeELStats(families: Family[]): ELStats {
const resistantsCount = families.filter(isResistant).length;
const resistantsOrEx = families.filter(
(f) => isResistant(f) || isExResistant(f)
);
const durations = resistantsOrEx.map((f) =>
daysInPeriod(periodOfResistance(f)!)
);
const dureeMoyenne = average(durations);
const dureeMediane = median(durations);
export function computeELStats(families: Famille[]): ELStats {
const actuelles = computeStatsActuelles(families);
const elYears = generateELYears();
const yearsStats = computeELPeriodStats(families, elYears);
@ -30,10 +14,7 @@ export function computeELStats(families: Family[]): ELStats {
const monthsStats = computeELPeriodStats(families, months);
return {
nbFamilleResistantes: resistantsCount,
nbFamilleResistantesOrEx: resistantsOrEx.length,
dureeMoyenneResistance: dureeMoyenne,
dureeMedianeResistance: dureeMediane,
actuelles: actuelles,
annees: yearsStats,
mois: monthsStats,
};

View File

@ -0,0 +1,37 @@
import {
Famille,
isExResistant,
isResistant,
periodOfResistance,
} from "../data/Famille";
import { daysInPeriod } from "../period/daysInPeriod";
import { ELStatsActuelles } from "./ELStats";
import { average } from "./math/average";
import { median } from "./math/median";
export function computeStatsActuelles(familles: Famille[]): ELStatsActuelles {
const resistantsCount = familles.filter((f) => isResistant(f)).length;
const resistantsOrEx = familles.filter(
(f) => isResistant(f) || isExResistant(f)
);
const durations = resistantsOrEx.map((f) =>
daysInPeriod(periodOfResistance(f)!)
);
const dureeMoyenne = average(durations);
const dureeMediane = median(durations);
const familleAvecMiseEnDemeure = resistantsOrEx.filter((f) =>
f.Evenements.find((e) => e.Type === "Mise en demeure de scolarisation")
);
const actuelles: ELStatsActuelles = {
nbFamilleResistantes: resistantsCount,
nbFamilleResistantesOrEx: resistantsOrEx.length,
dureeResistanceMoyenne: dureeMoyenne,
dureeResistanceMediane: dureeMediane,
nbFamillesMiseEnDemeure: familleAvecMiseEnDemeure.length,
pourcentageFamillesMisesEnDemeure:
(100 * familleAvecMiseEnDemeure.length) / resistantsOrEx.length,
};
return actuelles;
}