/** * llmindex.io — multilingual templates (checkable cross-lingual outputs, 0-999 range) * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * License: Proprietary — © Simon-Pierre Boucher, all rights reserved * * Hardened: full 0-999 number-word space in French (including the irregular * 70-99 zone: soixante-dix, quatre-vingts…) and Spanish (quinientos, * setecientos, novecientos…), both directions (number→words and words→number). * Hyphen/space orthography variants are normalized at grading time. */ import { ANSWER_FORMAT_INSTRUCTIONS } from '../answer'; import type { Rng } from '../rng'; import type { ItemTemplate } from '../types'; const FR_UNITS = [ 'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', 'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf', ]; function frBelowHundred(n: number): string { if (n < 20) return FR_UNITS[n]!; const tensNames: Record = { 20: 'vingt', 30: 'trente', 40: 'quarante', 50: 'cinquante', 60: 'soixante', }; if (n < 70) { const t = Math.floor(n / 10) * 10; const u = n % 10; if (u === 0) return tensNames[t]!; if (u === 1) return `${tensNames[t]!} et un`; return `${tensNames[t]!}-${FR_UNITS[u]!}`; } if (n < 80) { if (n === 71) return 'soixante et onze'; return `soixante-${FR_UNITS[n - 60]!}`; } if (n === 80) return 'quatre-vingts'; if (n < 100) return `quatre-vingt-${FR_UNITS[n - 80]!}`; throw new Error('frBelowHundred supports 0-99'); } /** French number words, 0-999. */ export function numToFrench(n: number): string { if (n < 0 || n > 999) throw new Error('numToFrench supports 0-999'); if (n < 100) return frBelowHundred(n); const hundreds = Math.floor(n / 100); const rest = n % 100; const hundredPart = hundreds === 1 ? 'cent' : rest === 0 ? `${FR_UNITS[hundreds]!} cents` : `${FR_UNITS[hundreds]!} cent`; return rest === 0 ? hundredPart : `${hundredPart} ${frBelowHundred(rest)}`; } const ES_UNITS = [ 'cero', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve', 'diez', 'once', 'doce', 'trece', 'catorce', 'quince', 'dieciséis', 'diecisiete', 'dieciocho', 'diecinueve', 'veinte', 'veintiuno', 'veintidós', 'veintitrés', 'veinticuatro', 'veinticinco', 'veintiséis', 'veintisiete', 'veintiocho', 'veintinueve', ]; function esBelowHundred(n: number): string { if (n < 30) return ES_UNITS[n]!; const tensNames: Record = { 30: 'treinta', 40: 'cuarenta', 50: 'cincuenta', 60: 'sesenta', 70: 'setenta', 80: 'ochenta', 90: 'noventa', }; const t = Math.floor(n / 10) * 10; const u = n % 10; return u === 0 ? tensNames[t]! : `${tensNames[t]!} y ${ES_UNITS[u]!}`; } const ES_HUNDREDS: Record = { 1: 'ciento', 2: 'doscientos', 3: 'trescientos', 4: 'cuatrocientos', 5: 'quinientos', 6: 'seiscientos', 7: 'setecientos', 8: 'ochocientos', 9: 'novecientos', }; /** Spanish number words, 0-999. */ export function numToSpanish(n: number): string { if (n < 0 || n > 999) throw new Error('numToSpanish supports 0-999'); if (n < 100) return esBelowHundred(n); if (n === 100) return 'cien'; const hundreds = Math.floor(n / 100); const rest = n % 100; return rest === 0 ? ES_HUNDREDS[hundreds]! : `${ES_HUNDREDS[hundreds]!} ${esBelowHundred(rest)}`; } export const multilingualNumword: ItemTemplate = { id: 'multilingual.numword-v2', domain: 'multilingual', description: 'Compute a 3-digit arithmetic result and spell it in French or Spanish — full 0-999 space including the irregular French 70-99 zone.', paramSpace: 2 * 900 * 900, render(rng: Rng, perturbSeed: string) { const lang = rng.pick(['French', 'Spanish'] as const); const a = rng.int(47, 499); const b = rng.int(47, 460); const sum = a + b; const expected = lang === 'French' ? numToFrench(sum) : numToSpanish(sum); return { templateId: this.id, domain: this.domain, prompt: `Compute ${a} + ${b}, then write the result out in ${lang} number words (lowercase). ` + `Answer with the ${lang} words only.\n\n${ANSWER_FORMAT_INSTRUCTIONS}`, answerKey: expected, grading: 'exact', perturbSeed, }; }, }; export const multilingualWordToNum: ItemTemplate = { id: 'multilingual.wordnum-v1', domain: 'multilingual', description: 'Reverse direction: read numbers spelled in French AND Spanish, combine them arithmetically, answer with digits.', paramSpace: 900 * 900, render(rng: Rng, perturbSeed: string) { const a = rng.int(61, 999); // includes the irregular French zone often const b = rng.int(31, 999); const op = rng.pick(['+', '−'] as const); const result = op === '+' ? a + b : a - b; return { templateId: this.id, domain: this.domain, prompt: `A number is written in French: « ${numToFrench(a)} ». Another is written in Spanish: « ${numToSpanish(b)} ». ` + `Compute (French number) ${op} (Spanish number). Answer with digits only.\n\n${ANSWER_FORMAT_INSTRUCTIONS}`, answerKey: String(result), grading: 'numeric', perturbSeed, }; }, };