/** * TypeScript mirror of the Python mode_params parser. * * A mode-params string encodes voting formula parameters in a compact format. * Example: "D30M50B.1G.2T.1" * * Supported codes: * D = duration_days (int) * M = majority_pct (int, 0-100) * B = base_exponent (float) * G = gradient_exponent (float) * C = constant_base (float) * S = smith_exponent (float) * T = techcomm_exponent (float) * N = ratio_multiplier (float) * R = is_ratio_mode (bool, 0 or 1) * * Values may start with a dot for decimals < 1, e.g. "B.1" means base_exponent=0.1. */ export interface ModeParams { duration_days: number majority_pct: number base_exponent: number gradient_exponent: number constant_base: number smith_exponent: number | null techcomm_exponent: number | null ratio_multiplier: number | null is_ratio_mode: boolean } type CodeType = 'int' | 'float' | 'bool' const CODES: Record = { D: { key: 'duration_days', type: 'int' }, M: { key: 'majority_pct', type: 'int' }, B: { key: 'base_exponent', type: 'float' }, G: { key: 'gradient_exponent', type: 'float' }, C: { key: 'constant_base', type: 'float' }, S: { key: 'smith_exponent', type: 'float' }, T: { key: 'techcomm_exponent', type: 'float' }, N: { key: 'ratio_multiplier', type: 'float' }, R: { key: 'is_ratio_mode', type: 'bool' }, } const PARAM_RE = /([A-Z])(\d*\.?\d+)/g function getDefaults(): ModeParams { return { duration_days: 30, majority_pct: 50, base_exponent: 0.1, gradient_exponent: 0.2, constant_base: 0.0, smith_exponent: null, techcomm_exponent: null, ratio_multiplier: null, is_ratio_mode: false, } } /** * Parse a mode-params string into a structured ModeParams object. * * @param paramsStr - Compact parameter string, e.g. "D30M50B.1G.2T.1" * @returns Parsed parameters with defaults for codes not found * @throws Error if an unrecognised code letter is found */ export function parseModeParams(paramsStr: string): ModeParams { const result = getDefaults() if (!paramsStr || !paramsStr.trim()) { return result } let match: RegExpExecArray | null PARAM_RE.lastIndex = 0 while ((match = PARAM_RE.exec(paramsStr)) !== null) { const code = match[1] const rawValue = match[2] if (!(code in CODES)) { throw new Error(`Code de parametre inconnu : '${code}'`) } const { key, type } = CODES[code] if (type === 'int') { ;(result as any)[key] = Math.floor(parseFloat(rawValue)) } else if (type === 'float') { ;(result as any)[key] = parseFloat(rawValue) } else if (type === 'bool') { ;(result as any)[key] = parseFloat(rawValue) !== 0 } } return result } /** * Encode a ModeParams object into a compact mode-params string. * * Only includes parameters that differ from defaults. * * @param params - Parameters to encode * @returns Compact string, e.g. "D30M50B.1G.2" */ export function encodeModeParams(params: Partial): string { const defaults = getDefaults() const parts: string[] = [] const codeEntries = Object.entries(CODES) as [string, { key: keyof ModeParams; type: CodeType }][] for (const [code, { key, type }] of codeEntries) { const value = params[key] if (value === undefined || value === null) continue if (value === defaults[key]) continue if (type === 'int') { parts.push(`${code}${value}`) } else if (type === 'float') { const numVal = value as number if (numVal < 1 && numVal > 0) { parts.push(`${code}${numVal.toString().replace(/^0/, '')}`) } else { parts.push(`${code}${numVal}`) } } else if (type === 'bool') { parts.push(`${code}${value ? 1 : 0}`) } } return parts.join('') } /** * Format a mode-params string for human display. * * @param paramsStr - Compact parameter string * @returns Human-readable description in French */ export function formatModeParams(paramsStr: string): string { const params = parseModeParams(paramsStr) const parts: string[] = [] parts.push(`Duree: ${params.duration_days} jours`) parts.push(`Majorite: ${params.majority_pct}%`) parts.push(`Base: ${params.base_exponent}`) parts.push(`Gradient: ${params.gradient_exponent}`) if (params.constant_base > 0) { parts.push(`Constante: ${params.constant_base}`) } if (params.smith_exponent !== null) { parts.push(`Smith: ${params.smith_exponent}`) } if (params.techcomm_exponent !== null) { parts.push(`TechComm: ${params.techcomm_exponent}`) } return parts.join(' | ') }