++ scelta di territori e proclamatori con ricerca
Al posto delle tendine (Assegna, Registro, editor delle piantine) un
campo in cui si scrive e compaiono le proposte più vicine: uguale, inizia
con, contiene, errore di battitura ("forse cercavi"); parole in qualsiasi
ordine, senza badare a maiuscole e accenti. Campo vuoto: la tendina
completa. Fatto anche per il telefono (16px, voci da 44px, scelta al tocco).
- Logica pura in resources/js/scelta.js con test node (tests/js).
- Opzioni dal server: Territorio/Proclamatore::opzioneScelta(); i
territori già assegnati si trovano ("già assegnato a …") ma non si
scelgono. Nomi cifrati ordinati e cercati decifrati, mai in SQL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
101
resources/js/scelta.js
Normal file
101
resources/js/scelta.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// ─── Scelta con ricerca: la logica, senza DOM ───────────────────────────────
|
||||
//
|
||||
// Le proposte per quello che si scrive in un campo di scelta (territorio,
|
||||
// proclamatore). Funzioni pure, provate da tests/js/scelta.test.mjs.
|
||||
//
|
||||
// Un'opzione è { valore, etichetta, dettaglio?, chiavi: [stringhe], motivo?,
|
||||
// speciale? }:
|
||||
// chiavi i campi in cui cercare, in ordine di importanza (cognome prima
|
||||
// del nome, numero prima della zona)
|
||||
// motivo l'opzione c'è ma non si può scegliere ("già assegnato a …"): si
|
||||
// mostra solo cercando, mai nella tendina completa
|
||||
// speciale sempre in cima e mai filtrata ("— nessun territorio —")
|
||||
//
|
||||
// Punteggio di ogni parola scritta, più basso = più vicino:
|
||||
// 0 uguale · 1 inizia con · 2 contiene · 3 a una lettera di distanza
|
||||
// ("forse cercavi"), più 0.1 per ogni posizione della chiave.
|
||||
// Devono trovare posto tutte le parole scritte, in qualsiasi ordine
|
||||
// ("mario ros" trova Rossi Mario).
|
||||
|
||||
export function normalizza(testo) {
|
||||
return (testo ?? '')
|
||||
.toString()
|
||||
.normalize('NFD')
|
||||
.replace(/[̀-ͯ]/g, '')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Distanza di modifica tra due parole, fermandosi appena supera `limite`. */
|
||||
export function distanza(a, b, limite = 2) {
|
||||
if (Math.abs(a.length - b.length) > limite) return limite + 1;
|
||||
let precedente = Array.from({ length: b.length + 1 }, (_, j) => j);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
const riga = [i];
|
||||
let minimo = i;
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const costo = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
riga[j] = Math.min(precedente[j] + 1, riga[j - 1] + 1, precedente[j - 1] + costo);
|
||||
minimo = Math.min(minimo, riga[j]);
|
||||
}
|
||||
if (minimo > limite) return limite + 1;
|
||||
precedente = riga;
|
||||
}
|
||||
return precedente[b.length];
|
||||
}
|
||||
|
||||
const PERFETTO = 0, INIZIA = 1, CONTIENE = 2, VICINO = 3;
|
||||
|
||||
function punteggioParola(parola, chiavi) {
|
||||
let migliore = Infinity;
|
||||
chiavi.forEach((chiave, posizione) => {
|
||||
if (chiave === '') return;
|
||||
let p = Infinity;
|
||||
if (chiave === parola) p = PERFETTO;
|
||||
else if (chiave.startsWith(parola) || chiave.split(/\s+/).some((w) => w.startsWith(parola))) p = INIZIA;
|
||||
else if (chiave.includes(parola)) p = CONTIENE;
|
||||
else if (parola.length >= 4) {
|
||||
// Errori di battitura: una lettera (due dalle 7 lettere in su), su
|
||||
// parole intere. Mai sui numeri brevi: "35" non deve trovare "36".
|
||||
const tolleranza = parola.length >= 7 ? 2 : 1;
|
||||
if (chiave.split(/\s+/).some((w) => distanza(w, parola, tolleranza) <= tolleranza)) p = VICINO;
|
||||
}
|
||||
if (p !== Infinity) migliore = Math.min(migliore, p + posizione * 0.1);
|
||||
});
|
||||
return migliore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Le opzioni da proporre per `testo`.
|
||||
* Campo vuoto: la tendina completa (speciali e sceglibili, nell'ordine dato).
|
||||
* Altrimenti: le opzioni in cui trovano posto tutte le parole, dalla più
|
||||
* vicina; `forse` segna quelle trovate solo per un errore di battitura.
|
||||
*/
|
||||
export function proponi(opzioni, testo, massimo = 50) {
|
||||
const speciali = opzioni.filter((o) => o.speciale);
|
||||
const normali = opzioni.filter((o) => !o.speciale);
|
||||
const parole = normalizza(testo).split(/\s+/).filter(Boolean);
|
||||
|
||||
if (parole.length === 0) {
|
||||
return [...speciali, ...normali.filter((o) => !o.motivo)].map((o) => ({ ...o, forse: false }));
|
||||
}
|
||||
|
||||
const trovate = [];
|
||||
normali.forEach((opzione, ordine) => {
|
||||
const chiavi = (opzione.chiavi ?? [opzione.etichetta]).map(normalizza);
|
||||
let totale = 0;
|
||||
let forse = false;
|
||||
for (const parola of parole) {
|
||||
const p = punteggioParola(parola, chiavi);
|
||||
if (p === Infinity) return;
|
||||
if (p >= VICINO) forse = true;
|
||||
totale += p;
|
||||
}
|
||||
trovate.push({ opzione, totale, ordine, forse });
|
||||
});
|
||||
|
||||
trovate.sort((a, b) => a.totale - b.totale || a.ordine - b.ordine);
|
||||
|
||||
return [...speciali.map((o) => ({ ...o, forse: false })),
|
||||
...trovate.slice(0, massimo).map((t) => ({ ...t.opzione, forse: t.forse }))];
|
||||
}
|
||||
Reference in New Issue
Block a user