// outils-imm1003.js — calculateurs du cours IMM1003 (Éléments d'évaluation immobilière) // Chaque outil : { id, title, ref, desc, formula (HTML), inputs[], compute(v) → [{label, value, fmt, cls}] } window.UQO_TOOLS = [ { id: "rpv", title: "Ratio prêt/valeur et mise de fonds", ref: { label: "Formule 1.1 · séance 1", url: "/seance/01/#form-1-1" }, desc: "Le prêteur retient la moindre du prix et de la valeur d’évaluation. Jusqu’à 80 % de RPV, le prêt est conventionnel ; au-delà, l’assurance prêt hypothécaire est obligatoire.", formula: "RPV = Prêt ÷ Valeur retenue × 100 · Valeur retenue = min(prix, évaluation)", inputs: [ { key: "prix", label: "Prix d’achat accepté", default: 450000, unit: "$", step: 1000 }, { key: "eval", label: "Valeur d’évaluation", default: 430000, unit: "$", step: 1000 }, { key: "mise", label: "Mise de fonds prévue (en % du prix)", default: 20, unit: "%", step: 0.5 }, { key: "rpvmax", label: "RPV maximal visé", default: 80, unit: "%", step: 1 }, ], compute(v) { const retenue = Math.min(v.prix, v.eval); const pretMax = retenue * v.rpvmax / 100; const miseReq = v.prix - pretMax; const misePrev = v.prix * v.mise / 100; const pretPrev = v.prix - misePrev; const rpvPrev = pretPrev / retenue * 100; return [ { label: "Valeur retenue par le prêteur (la moindre)", value: retenue }, { label: `Prêt maximal à ${v.rpvmax} % de RPV`, value: pretMax }, { label: "Mise de fonds requise (prix − prêt)", value: miseReq, cls: "main" }, { label: "Mise de fonds prévue", value: misePrev }, { label: "Écart à combler", value: miseReq - misePrev, cls: miseReq - misePrev > 0 ? "warn" : "good" }, { label: "RPV effectif avec la mise prévue", value: rpvPrev, fmt: "pct1", cls: rpvPrev > 80 ? "warn" : "good" }, { label: rpvPrev > 80 ? "Assurance prêt hypothécaire obligatoire (RPV > 80 %)" : "Prêt conventionnel (RPV ≤ 80 %)", value: rpvPrev > 80 ? "SCHL / Sagen / Canada Guaranty" : "aucune assurance requise", fmt: "text" }, ]; }, }, { id: "mutation", title: "Droits de mutation immobilière (barème 2026)", ref: { label: "Aide-mémoire · ratios", url: "/aide-memoire/" }, desc: "Barème général par tranches, appliqué au plus élevé du prix payé et de la valeur au rôle pondérée. Les municipalités peuvent majorer les tranches au-delà de 500 000 $ (jusqu’à 3 %) ; Montréal a son propre barème.", formula: "0,5 % jusqu’à 62 900 $ · 1,0 % de 62 900 $ à 315 000 $ · 1,5 % au-delà (+ taux municipal facultatif au-delà de 500 000 $)", inputs: [ { key: "base", label: "Base d’imposition (prix ou valeur au rôle pondérée, la plus élevée)", default: 477000, unit: "$", step: 1000 }, { key: "t1", label: "Seuil 1", default: 62900, unit: "$", step: 100 }, { key: "t2", label: "Seuil 2", default: 315000, unit: "$", step: 100 }, { key: "muni", label: "Taux municipal majoré au-delà de 500 000 $ (0 = barème de base)", default: 0, unit: "%", step: 0.25, min: 0, max: 3 }, ], compute(v) { const b = v.base; const tr1 = Math.min(b, v.t1) * 0.005; const tr2 = Math.max(0, Math.min(b, v.t2) - v.t1) * 0.01; let tr3, tr4 = 0; if (v.muni > 0 && b > 500000) { tr3 = Math.max(0, 500000 - v.t2) * 0.015; tr4 = (b - 500000) * v.muni / 100; } else { tr3 = Math.max(0, b - v.t2) * 0.015; } const total = tr1 + tr2 + tr3 + tr4; const out = [ { label: `Tranche 1 — 0,5 % sur ${new Intl.NumberFormat("fr-CA").format(Math.min(b, v.t1))} $`, value: tr1, fmt: "money2" }, { label: "Tranche 2 — 1,0 %", value: tr2, fmt: "money2" }, { label: "Tranche 3 — 1,5 %", value: tr3, fmt: "money2" }, ]; if (tr4) out.push({ label: `Tranche municipale — ${v.muni} % au-delà de 500 000 $`, value: tr4, fmt: "money2" }); out.push({ label: "Droits de mutation exigibles", value: total, fmt: "money2", cls: "main" }); out.push({ label: "Taux effectif", value: total / b * 100, fmt: "pct" }); return out; }, }, { id: "temporel", title: "Ajustement temporel d’un comparable", ref: { label: "Formule · séance 8", url: "/seance/08/" }, desc: "Corrige le prix d’un comparable pour l’évolution du marché entre sa date de vente et la date d’évaluation. C’est un ajustement transactionnel, appliqué avant les ajustements physiques.", formula: "Ajustement = Prix × gmensuel × nmois (linéaire) ou Prix × (1 + gmensuel)n − Prix (composé)", inputs: [ { key: "prix", label: "Prix de vente du comparable", default: 462000, unit: "$", step: 1000 }, { key: "g", label: "Taux de variation du marché (annuel)", default: 6, unit: "%/an", step: 0.5 }, { key: "mois", label: "Mois écoulés depuis la vente", default: 7, unit: "mois", step: 1, min: 0 }, ], compute(v) { const gm = v.g / 100 / 12; const lin = v.prix * gm * v.mois; const comp = v.prix * Math.pow(1 + gm, v.mois) - v.prix; return [ { label: "Taux mensuel équivalent", value: gm * 100, fmt: "pct3" }, { label: "Ajustement linéaire (méthode du cours)", value: lin, cls: "main" }, { label: "Prix ajusté (linéaire)", value: v.prix + lin }, { label: "Ajustement composé (variante)", value: comp }, { label: "Prix ajusté (composé)", value: v.prix + comp }, { label: "Poids de l’ajustement dans le prix", value: lin / v.prix * 100, fmt: "pct", cls: Math.abs(lin / v.prix) > 0.1 ? "warn" : "" }, ]; }, }, { id: "grille", title: "Grille de comparaison — trois comparables", ref: { label: "Séance 9 · ajustements et réconciliation", url: "/seance/09/" }, desc: "Saisissez le prix et les ajustements (du comparable vers le sujet : supérieur → négatif, inférieur → positif). L’outil calcule les prix ajustés, les ajustements net et brut, vérifie les seuils (net ≤ 15 %, brut ≤ 25 %) et propose une réconciliation pondérée par la fiabilité.", formula: "Prix ajusté = Prix × (1 + temps) + Σ ajustements $ · Net = Σ ajustements / prix · Brut = Σ |ajustements| / prix", inputs: [ { group: "Comparable 1" }, { key: "p1", label: "Prix", default: 445000, unit: "$", step: 1000 }, { key: "t1", label: "Ajustement temporel", default: 2, unit: "%", step: 0.5 }, { key: "a1", label: "Localisation", default: 0, unit: "$", step: 1000 }, { key: "b1", label: "Superficie / physique", default: -12000, unit: "$", step: 1000 }, { key: "c1", label: "Garage, sous-sol, extras", default: 8000, unit: "$", step: 1000 }, { group: "Comparable 2" }, { key: "p2", label: "Prix", default: 472000, unit: "$", step: 1000 }, { key: "t2", label: "Ajustement temporel", default: 1, unit: "%", step: 0.5 }, { key: "a2", label: "Localisation", default: -15000, unit: "$", step: 1000 }, { key: "b2", label: "Superficie / physique", default: -8000, unit: "$", step: 1000 }, { key: "c2", label: "Garage, sous-sol, extras", default: -10000, unit: "$", step: 1000 }, { group: "Comparable 3" }, { key: "p3", label: "Prix", default: 418000, unit: "$", step: 1000 }, { key: "t3", label: "Ajustement temporel", default: 3.5, unit: "%", step: 0.5 }, { key: "a3", label: "Localisation", default: 10000, unit: "$", step: 1000 }, { key: "b3", label: "Superficie / physique", default: 15000, unit: "$", step: 1000 }, { key: "c3", label: "Garage, sous-sol, extras", default: 12000, unit: "$", step: 1000 }, ], compute(v) { const rows = []; const res = []; let wsum = 0, vsum = 0; for (let i = 1; i <= 3; i++) { const p = v["p" + i]; const pt = p * (1 + v["t" + i] / 100); const adj = [v["a" + i], v["b" + i], v["c" + i]]; const net = adj.reduce((a, b) => a + b, 0) + (pt - p); const brut = adj.reduce((a, b) => a + Math.abs(b), 0) + Math.abs(pt - p); const pa = pt + adj.reduce((a, b) => a + b, 0); const netPct = net / p * 100, brutPct = brut / p * 100; const fiable = Math.abs(netPct) <= 15 && brutPct <= 25; const w = fiable ? 1 / Math.max(brutPct, 1) : 0.25 / Math.max(brutPct, 1); wsum += w; vsum += w * pa; rows.push([`C${i}`, p, pa, netPct, brutPct, fiable ? "✓" : "⚠ hors seuil"]); } const concl = vsum / wsum; const arrondi = Math.round(concl / 5000) * 5000; const vals = rows.map((r) => r[2]); res.push({ table: { head: ["Comp.", "Prix", "Prix ajusté", "Net", "Brut", "Seuils"], rows, fmt: [null, "money", "money", "pct1", "pct1", "text"] } }); res.push({ label: "Fourchette des indications", value: `${new Intl.NumberFormat("fr-CA").format(Math.round(Math.min(...vals)))} $ – ${new Intl.NumberFormat("fr-CA").format(Math.round(Math.max(...vals)))} $`, fmt: "text" }); res.push({ label: "Écart de la fourchette", value: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) * 100, fmt: "pct1", cls: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) > 0.15 ? "warn" : "good" }); res.push({ label: "Réconciliation pondérée par la fiabilité (poids ∝ 1/brut)", value: concl, cls: "main" }); res.push({ label: "Conclusion arrondie (au 5 000 $)", value: arrondi }); return res; }, }, { id: "rne", title: "Cascade des revenus : RBP → RBE → RNE → valeur", ref: { label: "Formules 11.x · séance 11", url: "/seance/11/" }, desc: "Reconstruisez le revenu net d’exploitation d’un immeuble locatif puis capitalisez-le. Le service de la dette, l’amortissement et l’impôt sont exclus ; la gestion s’impute même en autogestion ; la réserve pour remplacement fait partie des dépenses.", formula: "RBE = RBP − inoccupation − mauvaises créances + autres revenus · RNE = RBE − dépenses · V = RNE ÷ TGA", inputs: [ { key: "n", label: "Nombre de logements", default: 6, unit: "", step: 1, min: 1 }, { key: "loyer", label: "Loyer mensuel moyen du marché", default: 1050, unit: "$/mois", step: 25 }, { key: "autres", label: "Autres revenus annuels (stationnement, buanderie)", default: 2400, unit: "$/an", step: 100 }, { key: "inocc", label: "Inoccupation et mauvaises créances", default: 4, unit: "% du RBP", step: 0.5 }, { group: "Dépenses d’exploitation annuelles" }, { key: "taxes", label: "Taxes municipales et scolaires", default: 9800, unit: "$", step: 100 }, { key: "assur", label: "Assurances", default: 3600, unit: "$", step: 100 }, { key: "entretien", label: "Entretien et réparations", default: 5200, unit: "$", step: 100 }, { key: "energie", label: "Énergie (espaces communs) et déneigement", default: 3100, unit: "$", step: 100 }, { key: "gestion", label: "Gestion", default: 5, unit: "% du RBE", step: 0.5 }, { key: "reserve", label: "Réserve pour remplacement", default: 3, unit: "% du RBE", step: 0.5 }, { group: "Capitalisation" }, { key: "tga", label: "Taux global d’actualisation (TGA)", default: 5.5, unit: "%", step: 0.1 }, ], compute(v) { const rbp = v.n * v.loyer * 12; const pertes = rbp * v.inocc / 100; const rbe = rbp - pertes + v.autres; const gestion = rbe * v.gestion / 100; const reserve = rbe * v.reserve / 100; const dep = v.taxes + v.assur + v.entretien + v.energie + gestion + reserve; const rne = rbe - dep; const ratio = dep / rbe * 100; const val = rne / (v.tga / 100); return [ { label: "Revenu brut potentiel (RBP)", value: rbp }, { label: `− Inoccupation et mauvaises créances (${v.inocc} %)`, value: -pertes }, { label: "+ Autres revenus", value: v.autres }, { label: "= Revenu brut effectif (RBE)", value: rbe, cls: "good" }, { label: "Gestion", value: gestion }, { label: "Réserve pour remplacement", value: reserve }, { label: "− Dépenses d’exploitation totales", value: -dep }, { label: "Ratio dépenses / RBE (repère 35–45 % pour 6–24 logements)", value: ratio, fmt: "pct1", cls: ratio < 30 || ratio > 50 ? "warn" : "good" }, { label: "= Revenu net d’exploitation (RNE)", value: rne, cls: "good" }, { label: `Valeur par capitalisation directe à ${v.tga} %`, value: val, cls: "main" }, { label: "Valeur par logement", value: val / v.n }, { label: "MRB implicite (valeur / RBE)", value: val / rbe, fmt: "num" }, ]; }, }, { id: "tga", title: "TGA : extraction, bande d’investissement et sommation", ref: { label: "Formules 12.x · séance 12", url: "/seance/12/" }, desc: "Trois façons d’estimer le taux global d’actualisation : l’extraction sur une vente comparable (méthode reine), la bande d’investissement hypothèque–équité et la construction par sommation. Comparez-les.", formula: "TGA = RNE ÷ Prix · TGA = M·Rm + (1 − M)·Re · Rm = 12 × (i/12) / (1 − (1 + i/12)−n)", inputs: [ { group: "Extraction du marché" }, { key: "rne", label: "RNE du comparable", value: 0, default: 46800, unit: "$", step: 100 }, { key: "prix", label: "Prix de vente du comparable", default: 851000, unit: "$", step: 1000 }, { group: "Bande d’investissement" }, { key: "m", label: "Ratio prêt/valeur (M)", default: 75, unit: "%", step: 1 }, { key: "i", label: "Taux d’intérêt hypothécaire", default: 5.5, unit: "%", step: 0.05 }, { key: "amort", label: "Amortissement", default: 25, unit: "ans", step: 1 }, { key: "re", label: "Rendement exigé sur l’équité (Re)", default: 8, unit: "%", step: 0.25 }, { group: "Sommation" }, { key: "rf", label: "Taux sans risque (obligations 10 ans)", default: 3.2, unit: "%", step: 0.05 }, { key: "prisque", label: "Prime de risque immobilier", default: 2.0, unit: "%", step: 0.1 }, { key: "pliq", label: "Prime de non-liquidité", default: 1.0, unit: "%", step: 0.1 }, { key: "pgest", label: "Prime de gestion", default: 0.5, unit: "%", step: 0.1 }, { key: "g", label: "− Croissance anticipée des revenus", default: 1.5, unit: "%", step: 0.1 }, ], compute(v) { const ext = v.rne / v.prix * 100; const im = v.i / 100 / 12; const n = v.amort * 12; const rm = 12 * im / (1 - Math.pow(1 + im, -n)) * 100; const bande = v.m / 100 * rm + (1 - v.m / 100) * v.re; const somm = v.rf + v.prisque + v.pliq + v.pgest - v.g; const vals = [ext, bande, somm]; return [ { label: "TGA extrait du marché (RNE ÷ prix)", value: ext, fmt: "pct", cls: "main" }, { label: "Constante hypothécaire annuelle Rm", value: rm, fmt: "pct" }, { label: "TGA par bande d’investissement", value: bande, fmt: "pct" }, { label: "TGA par sommation (taux construit)", value: somm, fmt: "pct" }, { label: "Écart max. entre les trois méthodes", value: Math.max(...vals) - Math.min(...vals), fmt: "pct", cls: Math.max(...vals) - Math.min(...vals) > 1 ? "warn" : "good" }, { label: "Multiplicateur de revenu net implicite (1/TGA extrait)", value: 100 / ext, fmt: "num" }, ]; }, }, { id: "dcf", title: "Actualisation des flux monétaires (DCF)", ref: { label: "Formule DCF · séance 12", url: "/seance/12/" }, desc: "Valeur actuelle des RNE sur la période de détention plus la valeur de réversion (RNE de l’année n+1 capitalisé au TGA de sortie, moins les frais de vente). Observez le poids de la réversion et la sensibilité au taux.", formula: "V0 = Σ RNEt/(1+r)t + Vn/(1+r)n · Vn = RNEn+1 ÷ TGAsortie − frais de vente", inputs: [ { key: "rne1", label: "RNE de l’année 1", default: 128000, unit: "$", step: 1000 }, { key: "g", label: "Croissance annuelle du RNE", default: 2.5, unit: "%", step: 0.25 }, { key: "n", label: "Période de détention", default: 5, unit: "ans", step: 1, min: 1, max: 15 }, { key: "r", label: "Taux d’actualisation (r)", default: 7.5, unit: "%", step: 0.25 }, { key: "tgaout", label: "TGA de sortie", default: 5.75, unit: "%", step: 0.25 }, { key: "frais", label: "Frais de vente à la réversion", default: 3, unit: "% de la valeur brute", step: 0.5 }, ], compute(v) { const r = v.r / 100, g = v.g / 100; let pv = 0; const rows = []; let rne = v.rne1; for (let t = 1; t <= v.n; t++) { const f = 1 / Math.pow(1 + r, t); pv += rne * f; rows.push([`Année ${t}`, rne, f, rne * f]); rne *= 1 + g; } const rneN1 = rne; // RNE année n+1 const vBrute = rneN1 / (v.tgaout / 100); const vNette = vBrute * (1 - v.frais / 100); const pvRev = vNette / Math.pow(1 + r, v.n); const v0 = pv + pvRev; const capDirecte = v.rne1 / ((v.r - v.g) / 100); // sensibilité ±0,5 point sur r const sens = (dr) => { let s = 0, x = v.rne1; for (let t = 1; t <= v.n; t++) { s += x / Math.pow(1 + r + dr, t); x *= 1 + g; } return s + vNette / Math.pow(1 + r + dr, v.n); }; return [ { table: { head: ["Année", "RNE", "Facteur", "Valeur actuelle"], rows, fmt: [null, "money", "num4", "money"] } }, { label: "VA des RNE de la période", value: pv }, { label: `RNE de l’année ${v.n + 1} (base de la réversion)`, value: rneN1 }, { label: "Valeur de réversion brute", value: vBrute }, { label: "Valeur de réversion nette de frais", value: vNette }, { label: "VA de la réversion", value: pvRev }, { label: "Poids de la réversion dans la valeur", value: pvRev / v0 * 100, fmt: "pct1", cls: pvRev / v0 > 0.8 ? "warn" : "" }, { label: "Valeur par DCF", value: v0, cls: "main" }, { label: "Vérification : capitalisation directe au TGA ≈ r − g", value: capDirecte }, { label: "Sensibilité : r + 0,5 point", value: sens(0.005) }, { label: "Sensibilité : r − 0,5 point", value: sens(-0.005) }, ]; }, }, { id: "cout", title: "Méthode du coût simplifiée (âge/vie modifiée)", ref: { label: "Formules 10.x · séance 10", url: "/seance/10/" }, desc: "V = coût neuf − dépréciation + terrain. L’entretien différé (curable) est retiré de la base, puis le ratio âge effectif / vie utile s’applique au reste ; les dépréciations fonctionnelle et économique s’ajoutent.", formula: "V = Cneuf − Dtotale + Vterrain · Dphys. inc. = (Cneuf − curable) × âge effectif / vie utile", inputs: [ { key: "sup", label: "Superficie habitable", default: 1650, unit: "pi²", step: 10 }, { key: "cu", label: "Coût direct unitaire", default: 165, unit: "$/pi²", step: 5 }, { key: "ind", label: "Coûts indirects", default: 12, unit: "% des directs", step: 1 }, { key: "profit", label: "Profit de l’entrepreneur", default: 10, unit: "% des directs", step: 1 }, { key: "curable", label: "Entretien différé (curable)", default: 9500, unit: "$", step: 500 }, { key: "age", label: "Âge effectif", default: 18, unit: "ans", step: 1, min: 0 }, { key: "vie", label: "Vie utile économique", default: 60, unit: "ans", step: 5, min: 1 }, { key: "fonc", label: "Dépréciation fonctionnelle", default: 6000, unit: "$", step: 500 }, { key: "eco", label: "Dépréciation économique (externe)", default: 0, unit: "$", step: 500 }, { key: "terrain", label: "Valeur du terrain (vacant, utilisation optimale)", default: 145000, unit: "$", step: 1000 }, ], compute(v) { const directs = v.sup * v.cu; const indirects = directs * v.ind / 100; const profit = directs * v.profit / 100; const cneuf = directs + indirects + profit; const base = cneuf - v.curable; const inc = base * Math.min(1, v.age / v.vie); const dtot = v.curable + inc + v.fonc + v.eco; const val = cneuf - dtot + v.terrain; return [ { label: "Coûts directs", value: directs }, { label: "Coûts indirects", value: indirects }, { label: "Profit de l’entrepreneur", value: profit }, { label: "Coût à neuf", value: cneuf, cls: "good" }, { label: "Dépréciation physique curable (entretien différé)", value: v.curable }, { label: `Dépréciation physique incurable (${v.age}/${v.vie} de la base résiduelle)`, value: inc }, { label: "Dépréciation fonctionnelle + économique", value: v.fonc + v.eco }, { label: "Dépréciation totale", value: dtot }, { label: "Taux global de dépréciation", value: dtot / cneuf * 100, fmt: "pct1", cls: dtot >= cneuf ? "warn" : "" }, { label: "Valeur dépréciée des améliorations", value: cneuf - dtot }, { label: "+ Valeur du terrain", value: v.terrain }, { label: "Valeur par la méthode du coût", value: val, cls: "main" }, { label: "Arrondie au 5 000 $", value: Math.round(val / 5000) * 5000 }, ]; }, }, { id: "reconciliation", title: "Réconciliation pondérée des trois méthodes", ref: { label: "Séance 14 · réconciliation", url: "/seance/14/" }, desc: "La conclusion n’est jamais une moyenne : chaque indication est pondérée selon la pertinence de la méthode, la qualité des données et le comportement des acheteurs types. Les poids doivent totaliser 100 %.", formula: "Vconclue = Σ wi × Vi, avec Σ wi = 1", inputs: [ { key: "vc", label: "Indication — comparaison", default: 1580000, unit: "$", step: 5000 }, { key: "wc", label: "Poids — comparaison", default: 40, unit: "%", step: 5 }, { key: "vk", label: "Indication — coût", default: 1610000, unit: "$", step: 5000 }, { key: "wk", label: "Poids — coût", default: 15, unit: "%", step: 5 }, { key: "vr", label: "Indication — revenu", default: 1580000, unit: "$", step: 5000 }, { key: "wr", label: "Poids — revenu", default: 45, unit: "%", step: 5 }, { key: "arr", label: "Arrondi", default: 5000, unit: "$", step: 1000 }, ], compute(v) { const w = v.wc + v.wk + v.wr; const val = (v.vc * v.wc + v.vk * v.wk + v.vr * v.wr) / 100; const moy = (v.vc + v.vk + v.vr) / 3; const vals = [v.vc, v.vk, v.vr]; return [ { label: "Somme des poids", value: w, fmt: "pct1", cls: Math.abs(w - 100) > 0.01 ? "warn" : "good" }, { label: "Valeur pondérée", value: val * 100 / w, cls: "main" }, { label: `Conclusion arrondie (${new Intl.NumberFormat("fr-CA").format(v.arr)} $)`, value: Math.round(val * 100 / w / v.arr) * v.arr }, { label: "Moyenne simple (à titre indicatif seulement — jamais la conclusion)", value: moy }, { label: "Dispersion des indications (max − min) / min", value: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) * 100, fmt: "pct1", cls: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) > 0.1 ? "warn" : "good" }, ]; }, }, { id: "tvm", title: "Valeur temps de l’argent", ref: { label: "Formules VF / VA · séance 12", url: "/seance/12/" }, desc: "Un dollar reçu aujourd’hui vaut plus qu’un dollar reçu demain. Capitalisation d’une somme, actualisation d’un montant futur et constante hypothécaire.", formula: "FV = PV × (1 + r)n · PV = FV ÷ (1 + r)n", inputs: [ { key: "pv", label: "Montant présent (PV)", default: 100000, unit: "$", step: 1000 }, { key: "fv", label: "Montant futur à actualiser (FV)", default: 150000, unit: "$", step: 1000 }, { key: "r", label: "Taux annuel", default: 5, unit: "%", step: 0.25 }, { key: "n", label: "Nombre d’années", default: 10, unit: "ans", step: 1, min: 0 }, ], compute(v) { const r = v.r / 100; const fv = v.pv * Math.pow(1 + r, v.n); const pv = v.fv / Math.pow(1 + r, v.n); const doubling = Math.log(2) / Math.log(1 + r); return [ { label: `Valeur future de ${new Intl.NumberFormat("fr-CA").format(v.pv)} $ dans ${v.n} ans`, value: fv, cls: "main" }, { label: `Valeur présente de ${new Intl.NumberFormat("fr-CA").format(v.fv)} $ reçus dans ${v.n} ans`, value: pv }, { label: "Facteur de capitalisation (1 + r)ⁿ", value: Math.pow(1 + r, v.n), fmt: "num4" }, { label: "Facteur d’actualisation 1/(1 + r)ⁿ", value: 1 / Math.pow(1 + r, v.n), fmt: "num4" }, { label: "Temps de doublement au taux r", value: doubling, fmt: "num" }, ]; }, }, { id: "unites", title: "Conversions d’unités et prix unitaires", ref: { label: "Aide-mémoire · conversions", url: "/aide-memoire/" }, desc: "Pi² ↔ m², acres ↔ hectares, et prix unitaires cohérents entre les systèmes.", formula: "1 m² ≈ 10,764 pi² · 1 acre ≈ 43 560 pi² ≈ 4 047 m² · 1 ha = 10 000 m²", inputs: [ { key: "pi2", label: "Superficie", default: 1650, unit: "pi²", step: 10 }, { key: "prix", label: "Prix total", default: 477000, unit: "$", step: 1000 }, { key: "acres", label: "Terrain", default: 0.5, unit: "acre", step: 0.05 }, ], compute(v) { const m2 = v.pi2 / 10.7639; return [ { label: "Superficie en m²", value: m2, fmt: "num" }, { label: "Prix au pi²", value: v.prix / v.pi2, fmt: "money2", cls: "main" }, { label: "Prix au m²", value: v.prix / m2, fmt: "money2" }, { label: "Terrain en pi²", value: v.acres * 43560, fmt: "int" }, { label: "Terrain en m²", value: v.acres * 4046.86, fmt: "int" }, { label: "Terrain en hectares", value: v.acres * 0.404686, fmt: "num4" }, ]; }, }, ];