SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
5.3 KB · 144 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Moteur de conversion d'unités de la base de coûts. Module PUR.4 * Règle : on conserve TOUJOURS l'unité d'origine (source_unit), l'unité5 * canonique (canonical_unit) et le facteur de conversion appliqué.6 */7import type { CanonicalUnit } from "./types";89export const SQFT_PER_M2 = 10.7639;10export const M2_PER_SQFT = 0.092903;11export const FT_PER_M = 3.28084;12export const M3_PER_FT3 = 0.0283168;13export const LB_PER_KG = 2.20462;14export const L_PER_GAL_US = 3.78541;15export const L_PER_GAL_IMP = 4.54609;1617/** Libellés FR/EN des unités canoniques. */18export const UNIT_LABELS: Record<CanonicalUnit, { fr: string; en: string }> = {19  unit: { fr: "unité", en: "each" },20  pi2: { fr: "pi²", en: "sq ft" },21  pi_lin: { fr: "pi lin.", en: "lin ft" },22  pi3: { fr: "pi³", en: "cu ft" },23  m2: { fr: "m²", en: "m²" },24  m3: { fr: "m³", en: "m³" },25  kg: { fr: "kg", en: "kg" },26  lb: { fr: "lb", en: "lb" },27  L: { fr: "L", en: "L" },28  gal: { fr: "gal", en: "gal" },29  h: { fr: "heure", en: "hour" },30  day: { fr: "jour", en: "day" },31  lump: { fr: "forfait", en: "lump sum" },32};3334/** Unités source reconnues (normalisées en minuscules, sans accents). */35export type SourceUnit =36  | CanonicalUnit37  | "chaque"38  | "each"39  | "ea"40  | "paquet"41  | "pack"42  | "boite"43  | "box"44  | "sac"45  | "bag"46  | "rouleau"47  | "roll"48  | "feuille"49  | "sheet"50  | "seau"51  | "pail"52  | "planche"53  | "board"54  | "longueur"55  | "length"56  | "ballot"57  | "bundle"58  | "verge3"59  | "cu_yd"60  | "tonne"61  | "kit";6263export interface UnitConversion {64  sourceUnit: string;65  canonicalUnit: CanonicalUnit;66  /** prix canonique = prix source × factor (ex. feuille 32 pi² → factor 1/32) */67  factor: number;68  method: string;69}7071export function normalizeUnitToken(raw: string): string {72  return raw73    .normalize("NFD")74    .replace(/[̀-ͯ]/g, "")75    .toLowerCase()76    .replace(/[^a-z0-9/]/g, "")77    .trim();78}7980/** Unités « emballage » qui nécessitent une quantité par emballage propre à l'article. */81const PACK_UNITS = new Set(["paquet", "pack", "boite", "box", "sac", "bag", "rouleau", "roll", "feuille", "sheet", "seau", "pail", "planche", "board", "longueur", "length", "ballot", "bundle", "kit", "pqt", "bte"]);8283const DIRECT: Record<string, CanonicalUnit> = {84  unit: "unit", chaque: "unit", each: "unit", ea: "unit", ch: "unit", un: "unit", unite: "unit",85  pi2: "pi2", pi: "pi_lin", sqft: "pi2", "pi²": "pi2", pieds2: "pi2", piedcarre: "pi2", ft2: "pi2",86  pilin: "pi_lin", lf: "pi_lin", linft: "pi_lin", pl: "pi_lin", ft: "pi_lin",87  pi3: "pi3", ft3: "pi3", m2: "m2", m3: "m3", kg: "kg", lb: "lb", lbs: "lb", l: "L", litre: "L", liter: "L",88  gal: "gal", gallon: "gal", h: "h", heure: "h", hour: "h", hr: "h", day: "day", jour: "day", lump: "lump", forfait: "lump",89};9091/**92 * Convertit un prix exprimé dans une unité source vers l'unité canonique de93 * l'article. `packQty` = contenu d'un emballage exprimé dans l'unité canonique94 * (ex. feuille de gypse 4×8 → 32 pi² ; paquet d'isolant R24 → 78,3 pi²).95 */96export function convertPrice(97  price: number,98  sourceUnitRaw: string,99  canonical: CanonicalUnit,100  packQty?: number | null101): { price: number; conversion: UnitConversion } {102  const tok = normalizeUnitToken(sourceUnitRaw);103  const direct = DIRECT[tok];104  if (direct === canonical) {105    return { price, conversion: { sourceUnit: sourceUnitRaw, canonicalUnit: canonical, factor: 1, method: "identité" } };106  }107  // conversions métriques ↔ impériales simples108  const pair = `${direct ?? tok}->${canonical}`;109  const SIMPLE: Record<string, [number, string]> = {110    "m2->pi2": [1 / SQFT_PER_M2, "1 m² = 10,764 pi²"],111    "pi2->m2": [SQFT_PER_M2, "1 m² = 10,764 pi²"],112    "m3->pi3": [M3_PER_FT3, "1 pi³ = 0,0283 m³"],113    "pi3->m3": [1 / M3_PER_FT3, "1 pi³ = 0,0283 m³"],114    "verge3->m3": [1 / 0.764555, "1 vg³ = 0,7646 m³"],115    "cu_yd->m3": [1 / 0.764555, "1 vg³ = 0,7646 m³"],116    "kg->lb": [1 / LB_PER_KG, "1 kg = 2,2046 lb"],117    "lb->kg": [LB_PER_KG, "1 kg = 2,2046 lb"],118    "gal->L": [1 / L_PER_GAL_US, "1 gal US = 3,785 L"],119    "L->gal": [L_PER_GAL_US, "1 gal US = 3,785 L"],120    "m->pi_lin": [1 / FT_PER_M, "1 m = 3,2808 pi"],121    "pi_lin->m": [FT_PER_M, "1 m = 3,2808 pi"],122  };123  const s = SIMPLE[pair];124  if (s) return { price: price * s[0], conversion: { sourceUnit: sourceUnitRaw, canonicalUnit: canonical, factor: s[0], method: s[1] } };125  // emballages : besoin de la quantité par emballage126  if ((PACK_UNITS.has(tok) || direct === "unit" || tok === "") && packQty && packQty > 0) {127    const f = 1 / packQty;128    return {129      price: price * f,130      conversion: { sourceUnit: sourceUnitRaw, canonicalUnit: canonical, factor: f, method: `1 ${sourceUnitRaw || "emballage"} = ${packQty} ${UNIT_LABELS[canonical].fr}` },131    };132  }133  throw new Error(`Conversion impossible : « ${sourceUnitRaw} » → ${canonical}${packQty ? "" : " (quantité par emballage inconnue)"}`);134}135136/** Vrai si l'unité source est reconnue (canonique, métrique/impériale ou emballage). */137export function isKnownUnit(raw: string): boolean {138  const tok = normalizeUnitToken(raw);139  return tok in DIRECT || PACK_UNITS.has(tok) || ["m", "verge3", "cu_yd", "tonne"].includes(tok);140}141142export const sqftFromM2 = (m2: number): number => m2 * SQFT_PER_M2;143export const m2FromSqft = (sqft: number): number => sqft * M2_PER_SQFT;144