diff --git a/src/data/Famille.ts b/src/data/Famille.ts new file mode 100644 index 0000000..f4d3adc --- /dev/null +++ b/src/data/Famille.ts @@ -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); +} diff --git a/src/data/Family.ts b/src/data/Family.ts deleted file mode 100644 index aa5e43b..0000000 --- a/src/data/Family.ts +++ /dev/null @@ -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; -} diff --git a/src/data/checkDataConsistency.ts b/src/data/checkDataConsistency.ts index 90ead30..320212a 100644 --- a/src/data/checkDataConsistency.ts +++ b/src/data/checkDataConsistency.ts @@ -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") { diff --git a/src/index.ts b/src/index.ts index ff8d4c5..e7d0880 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); })(); diff --git a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts index bd2e50d..27c8dc2 100644 --- a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts +++ b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts @@ -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 { +): Promise { 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, diff --git a/src/notion/publish/publishPeriodStats.ts b/src/notion/publish/publishPeriodStats.ts new file mode 100644 index 0000000..14f30db --- /dev/null +++ b/src/notion/publish/publishPeriodStats.ts @@ -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}%)`; + } +} diff --git a/src/notion/publish/publishStatisticsToNotion.ts b/src/notion/publish/publishStatisticsToNotion.ts index 52fabca..c7991c9 100644 --- a/src/notion/publish/publishStatisticsToNotion.ts +++ b/src/notion/publish/publishStatisticsToNotion.ts @@ -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}%)`; - } -} diff --git a/src/notion/publish/publishCurrentStats.ts b/src/notion/publish/publishStatsActuelles.ts similarity index 65% rename from src/notion/publish/publishCurrentStats.ts rename to src/notion/publish/publishStatsActuelles.ts index ff610ed..755e467 100644 --- a/src/notion/publish/publishCurrentStats.ts +++ b/src/notion/publish/publishStatsActuelles.ts @@ -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 ), ], }); diff --git a/src/notion/publish/statNames.ts b/src/notion/publish/statNames.ts new file mode 100644 index 0000000..0dfdf6a --- /dev/null +++ b/src/notion/publish/statNames.ts @@ -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"; diff --git a/src/notion/utils/types/CreatePageProperties.ts b/src/notion/utils/types/CreatePageProperties.ts new file mode 100644 index 0000000..33b5d03 --- /dev/null +++ b/src/notion/utils/types/CreatePageProperties.ts @@ -0,0 +1,3 @@ +import { CreatePageParameters } from "@notionhq/client/build/src/api-endpoints"; + +export type CreatePageProperties = CreatePageParameters["properties"]; diff --git a/src/period/Period.ts b/src/period/Period.ts index cac010e..d895da3 100644 --- a/src/period/Period.ts +++ b/src/period/Period.ts @@ -1,5 +1,4 @@ export type Period = { start: Date; - /** Exclusive */ end: Date; }; diff --git a/src/period/isPeriodContaining.test.ts b/src/period/isPeriodContaining.test.ts index 659ff44..b419514 100644 --- a/src/period/isPeriodContaining.test.ts +++ b/src/period/isPeriodContaining.test.ts @@ -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( diff --git a/src/period/isPeriodContaining.ts b/src/period/isPeriodContaining.ts index 57df233..90929b4 100644 --- a/src/period/isPeriodContaining.ts +++ b/src/period/isPeriodContaining.ts @@ -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 ( diff --git a/src/statistiques/ELStats.ts b/src/statistiques/ELStats.ts index 5f3ee95..8fc1e12 100644 --- a/src/statistiques/ELStats.ts +++ b/src/statistiques/ELStats.ts @@ -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 = { diff --git a/src/statistiques/computeELPeriodStats.ts b/src/statistiques/computeELPeriodStats.ts index b0c1d5e..699a4ef 100644 --- a/src/statistiques/computeELPeriodStats.ts +++ b/src/statistiques/computeELPeriodStats.ts @@ -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(value: TValue | null): value is TValue { + return value !== null; +} diff --git a/src/statistiques/computeELStats.ts b/src/statistiques/computeELStats.ts index 32e4437..2f72b1d 100644 --- a/src/statistiques/computeELStats.ts +++ b/src/statistiques/computeELStats.ts @@ -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, }; diff --git a/src/statistiques/computeStatsActuelles.ts b/src/statistiques/computeStatsActuelles.ts new file mode 100644 index 0000000..6c9e7c8 --- /dev/null +++ b/src/statistiques/computeStatsActuelles.ts @@ -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; +}