Skip to content

Commit

Permalink
feat(rules): add new alternative garder sa voiture + refactor the J…
Browse files Browse the repository at this point in the history
…S API
  • Loading branch information
EmileRolley committed Jan 6, 2025
1 parent 9591cf9 commit 3be85f9
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ test.ts
scripts/data/quechoisir/*.html
dist/
publicodes-build/
src/rules/alternatives.publicodes
src/rules/alternatives/achat-voiture.publicodes
src/personas/personas.json
9 changes: 3 additions & 6 deletions scripts/generate-alternatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* motorisations
*/

const ALTERNATIVES_VOITURE_NAMESPACE = "alternatives . voiture"
const ALTERNATIVES_VOITURE_NAMESPACE = "alternatives . acheter voiture"

export default function generateAlternatives(rules) {
const carburants = Object.keys(rules).flatMap((key) => {
Expand Down Expand Up @@ -35,11 +35,8 @@ export default function generateAlternatives(rules) {
})

const alternatives = {
alternatives: {
titre: "Alternatives",
},
"alternatives . voiture": {
titre: "Voiture Individuelle",
"alternatives . acheter voiture": {
titre: "Acheter une nouvelle voiture",
},
}

Expand Down
4 changes: 2 additions & 2 deletions scripts/precompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ROOT_PATH = new URL(".", import.meta.url).pathname
const SRC_FILES = join(ROOT_PATH, "../src/rules/")
const ALTERNATIVES_DEST_PATH = join(
ROOT_PATH,
"../src/rules/alternatives.publicodes",
"../src/rules/alternatives/achat-voiture.publicodes",
)
const PERSONAS_DEST_PATH = join(ROOT_PATH, "../src/personas/personas.json")

Expand All @@ -26,13 +26,13 @@ const resolvedRules = Object.fromEntries(
)

const alternatives = generateAlternatives(resolvedRules)
console.log(`✅ './src/rules/alternatives.publicodes' generated`)
writeFileSync(
ALTERNATIVES_DEST_PATH,
`# GENERATED FILE - DO NOT EDIT\n\n${stringify(alternatives, {
aliasDuplicateObjects: false,
})}`,
)
console.log(`✅ './src/rules/alternatives/achat-voiture.publicodes' generated`)

const personas = getPersonas(resolvedRules)
writeFileSync(PERSONAS_DEST_PATH, JSON.stringify(personas))
Expand Down
143 changes: 87 additions & 56 deletions src/CarSimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ export type TargetInfos = {
}

/**
* Models an alternative to the current car (i.e. defined by the inputs).
* This is used to compare the current car with other alternatives.
*
* @note For now, the only alternative is a car, but in the future, we might
* have other alternatives like public transport, bike, etc.
* Models an alternative to the current car (i.e. defined by the inputs). This
* is used to compare the current car with other alternatives.
*/
export type Alternative = {
kind: "car"
} & EvaluatedCarInfos
export type Alternative =
| ({ kind: "buy-new-car" } & EvaluatedCarInfos)
| ({
kind: "keep-current-car"
/** The additional period (in years) to keep the car */
additionalPeriod: EvaluatedRuleInfos<
RuleValue["alternatives . garder sa voiture . durée supplémentaire"]
>
} & EvaluatedCarInfos)

/**
* Full information about an evaluated value.
Expand Down Expand Up @@ -83,10 +86,12 @@ const engineLogger = {
}

export const RULE_NAMES = Object.keys(rules) as RuleName[]
export const ALTERNATIVES_VOITURE_NAMESPACE: RuleName = "alternatives . voiture"
export const ALTERNATIVES_NAMESPACE: RuleName = "alternatives"
export const ALTERNATIVES_RULES = RULE_NAMES.filter(
(rule) =>
rule.startsWith(ALTERNATIVES_VOITURE_NAMESPACE) &&
rule.startsWith(ALTERNATIVES_NAMESPACE) &&
// NOTE: this is a bit hacking, we should probably add a field in the
// publicodes rules to mark the rules that are alternatives.
`${rule} . coûts` in rules &&
`${rule} . empreinte` in rules,
)
Expand Down Expand Up @@ -193,55 +198,81 @@ export class CarSimulator {
* @note This method is an expensive operation.
*/
public evaluateAlternatives(): Alternative[] {
const infos = ALTERNATIVES_RULES.map((rule: RuleName) => {
const splittedRule = rule.split(" . ").slice(2)
const sizeOption = splittedRule[0] as Questions["voiture . gabarit"]
const motorisationOption =
splittedRule[1] as Questions["voiture . motorisation"]
const fuelOption = (
motorisationOption !== "électrique" ? splittedRule[2] : undefined
) as Questions["voiture . thermique . carburant"]

if (!sizeOption || !motorisationOption) {
throw new Error(
`Invalid alternative rule ${rule}. It should have a size and a motorisation option.`,
)
return ALTERNATIVES_RULES.map((rule: RuleName) => {
if (rule.includes("acheter voiture")) {
return this.evaluateBuyNewCarAlternative(rule)
} else if (rule.includes("garder sa voiture")) {
return this.evaluateKeepCurrentCarAlternative(rule)
}
}).filter((alternative) => alternative !== undefined)
}

return {
kind: "car",
title: this.engine.getRule(rule).title,
cost: this.evaluateRule(ruleName(rule, "coûts")),
emissions: this.evaluateRule(ruleName(rule, "empreinte")),
size: {
value: sizeOption,
title: this.engine.getRule(ruleName("voiture . gabarit", sizeOption))
.title,
isEnumValue: true,
isApplicable: true,
},
motorisation: {
value: motorisationOption,
title: this.engine.getRule(
ruleName("voiture . motorisation", motorisationOption),
).title,
isEnumValue: true,
isApplicable: true,
},
fuel: fuelOption
? {
value: fuelOption,
title: this.engine.getRule(
ruleName("voiture . thermique . carburant", fuelOption),
).title,
isEnumValue: true,
isApplicable: true,
}
: undefined,
} as Alternative
})
private evaluateBuyNewCarAlternative(rule: RuleName): Alternative {
const splittedRule = rule.split(" . ").slice(2)
const sizeOption = splittedRule[0] as Questions["voiture . gabarit"]
const motorisationOption =
splittedRule[1] as Questions["voiture . motorisation"]
const fuelOption = (
motorisationOption !== "électrique" ? splittedRule[2] : undefined
) as Questions["voiture . thermique . carburant"]

if (!sizeOption || !motorisationOption) {
throw new Error(
`Invalid alternative rule ${rule}. It should have a size and a motorisation option.`,
)
}

return {
kind: "buy-new-car",
title: this.engine.getRule(rule).title,
cost: this.evaluateRule(ruleName(rule, "coûts")),
emissions: this.evaluateRule(ruleName(rule, "empreinte")),
size: {
value: sizeOption,
title: this.engine.getRule(ruleName("voiture . gabarit", sizeOption))
.title,
isEnumValue: true,
isApplicable: true,
},
motorisation: {
value: motorisationOption,
title: this.engine.getRule(
ruleName("voiture . motorisation", motorisationOption),
).title,
isEnumValue: true,
isApplicable: true,
},
fuel: fuelOption
? {
value: fuelOption,
title: this.engine.getRule(
ruleName("voiture . thermique . carburant", fuelOption),
).title,
isEnumValue: true,
isApplicable: true,
}
: undefined,
} as Alternative
}

private evaluateKeepCurrentCarAlternative(rule: RuleName): Alternative {
const additionalPeriod = this.evaluateRule(
ruleName(rule, "durée supplémentaire"),
)

return infos
return {
kind: "keep-current-car",
title: this.engine.getRule(rule).title,
cost: this.evaluateRule(ruleName(rule, "coûts")),
emissions: this.evaluateRule(ruleName(rule, "empreinte")),
additionalPeriod,
size: this.evaluateRule("voiture . gabarit"),
motorisation: this.evaluateRule("voiture . motorisation"),
fuel:
this.evaluateRule("voiture . motorisation").value !== "électrique"
? this.evaluateRule("voiture . thermique . carburant")
: undefined,
} as Alternative
}

/**
Expand Down
60 changes: 60 additions & 0 deletions src/rules/alternatives/alternatives.publicodes
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
alternatives:
titre: Alternatives

alternatives . garder sa voiture:
titre: Conserver sa voiture actuelle 5 ans de plus
description: >
Conserver sa voiture actuelle peut permettre de réduire les coûts annuels
liés à votre voiture. En effet, il n'y aura pas de coûts supplémentaires
liés à l'achat d'une nouvelle voiture et vous étendrez la durée
d'amortissement de l'achat de votre voiture actuelle.
Cependant, il est important de vérifier que votre voiture actuelle respecte
les [normes environnementales en
vigueur](https://www.ecologie.gouv.fr/politiques-publiques/zones-faibles-emissions-zfe).
avec:
empreinte:
valeur: empreinte
note: >
La durée de détention de la voiture n'impacte pas l'empreinte carbone.
En effet, nous utilisons le modèle de [Nos Gestes
Climat](https://nosgestesclimat.fr) qui a fait le choix d'amortir
l'empreinte carbone sur la durée de vie du véhicule en km parcourus.
Cela signifie que seule la distance parcourue en une année détermine
l'empreinte carbone annuelle de la voiture et permet de répartir
l'amortissement de l'empreinte de la construction équitablement entre
toustes les propriétaires successif:ves.
Voir la documentation sur
[l'amortissement](https://nosgestesclimat.fr/documentation/transport/voiture/amortissement)
pour plus d'informations.
coûts:
# TODO: prendre en compte l'augmentation des coûts d'entretien de la voiture
valeur: coûts
contexte:
voiture . durée de détention totale: voiture . durée de détention totale + durée supplémentaire

durée supplémentaire:
titre: Durée de détention supplémentaire
question: Combien de temps souhaitez-vous garder votre voiture actuelle ?
# TODO: déterminer la valeur à ajouter pour la détention de la voiture
par défaut: 5
unité: an

# alternatives . utiliser le vélo:
# applicable si:
# toutes ces conditions:
# - usage . km annuels . calculés
# - usage . km annuels . calculés . quotidien <= 15
# titre: Utiliser le vélo pour les trajets quotidiens
# description: >
# Utiliser le vélo est une option qui peut être intéressante si vous avez des
# trajets courts à effectuer. Cela vous permet de vous déplacer de manière
# écologique et de faire du sport en même temps.
#
# avec:
# empreinte:
# valeur: empreinte
15 changes: 13 additions & 2 deletions test/CarSimulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,10 @@ describe("CarSimulator", () => {
// Electrique
nbSizes

expect(alternatives).toHaveLength(nbAlternatives)
expect(alternatives).toHaveLength(
nbAlternatives + 1 /* Garder sa voiture */,
)
alternatives.forEach((alternative) => {
expect(alternative.kind).toEqual("car")
expect(alternative.cost.value).toBeGreaterThan(0)
expect(alternative.emissions.value).toBeGreaterThan(0)
expect(alternative.size.value).toBeDefined()
Expand All @@ -299,6 +300,16 @@ describe("CarSimulator", () => {
} else {
expect(alternative.fuel).toBeUndefined()
}

if (alternative.kind === "keep-current-car") {
expect(alternative.additionalPeriod).toEqual({
isApplicable: true,
isEnumValue: false,
title: "Durée de détention supplémentaire",
value: 5,
unit: "an",
})
}
})
})
})
Expand Down
68 changes: 68 additions & 0 deletions test/alternatives.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Engine from "publicodes"
import rules, { RuleName } from "../publicodes-build"
import { expect, test, describe } from "vitest"

describe("Alternatives", () => {
const engine = new Engine<RuleName>(rules, {
logger: {
log: () => {},
warn: () => {},
error: (message: string) => console.error(message),
},
})

describe("Garder sa voiture actuelle", () => {
test("diminue les coûts annuels", () => {
const coutsDeBase = engine.evaluate("coûts").nodeValue?.valueOf()
const acoutsAlternative = engine
.evaluate("alternatives . garder sa voiture . coûts")
.nodeValue?.valueOf()

if (
typeof coutsDeBase === "number" &&
typeof acoutsAlternative === "number"
) {
expect(acoutsAlternative).toBeLessThan(coutsDeBase)
} else {
expect(false, "les coûts doivent être des nombres").toBeTruthy()
}
})

test("garde la même empreinte", () => {
const empreinteDeBase = engine.evaluate("empreinte").nodeValue?.valueOf()
const empreinteAlternative = engine
.evaluate("alternatives . garder sa voiture . empreinte")
.nodeValue?.valueOf()

if (
typeof empreinteDeBase === "number" &&
typeof empreinteAlternative === "number"
) {
expect(empreinteAlternative).toEqual(empreinteDeBase)
} else {
expect(false, "les empreintes doivent être des nombres").toBeTruthy()
}
})

test("plus la durée de détention est longue, plus les coûts annuels diminuent", () => {
const coutsAlternative5ans = engine
.evaluate("alternatives . garder sa voiture . coûts")
.nodeValue?.valueOf()
const coutsAlternative7ans = engine
.setSituation({
"alternatives . garder sa voiture . durée supplémentaire": 7,
})
.evaluate("alternatives . garder sa voiture . coûts")
.nodeValue?.valueOf()

if (
typeof coutsAlternative5ans === "number" &&
typeof coutsAlternative7ans === "number"
) {
expect(coutsAlternative7ans).toBeLessThan(coutsAlternative5ans)
} else {
expect(false, "les coûts doivent être des nombres").toBeTruthy()
}
})
})
})

0 comments on commit 3be85f9

Please sign in to comment.