app: Désuffixage des mots de la recherche

This commit is contained in:
Mattéo Delabre 2019-12-04 17:59:52 -05:00
parent dc749885bd
commit c2fb067c77
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
3 changed files with 136 additions and 51 deletions

View File

@ -1,5 +1,6 @@
import React, {useState, useRef} from 'react'; import React, {useState, useRef} from 'react';
import * as diacritics from 'diacritics'; import * as fuzzy from '../data/fuzzy.js';
import * as util from '../util.js';
/** /**
* Codes des touches du clavier par nom. * Codes des touches du clavier par nom.
@ -13,41 +14,6 @@ const keys = Object.assign(Object.create(null), {
'down': 40, 'down': 40,
}); });
/**
* Met le nom dun terme sous forme normalisée pour la recherche approximative.
*
* Dans la forme normalisée, tous les accents sont ôtés et la casse est réduite
* en minuscules.
*
* @param name Nom à normaliser.
* @return Liste des mots du nom de terme normalisé.
*/
const normalizeName = name =>
diacritics.remove(name.toLowerCase())
.split(/\s+/g);
/**
* Vérifie si un terme est similaire à un préfixe saisi.
*
* @param prefix Préfixe saisi.
* @param term Terme à vérifier.
* @return Vrai si le terme est considéré comme similaire au préfixe.
*/
const termMatchesPrefix = (prefix, {name, alias}) =>
{
const haystack = [name].concat(alias)
.map(normalizeName)
.reduce((prev, next) => prev.concat(next), []);
const needle = normalizeName(prefix);
return needle.every(item =>
haystack.some(word =>
word.startsWith(item)
)
);
};
/** /**
* Zone de saisie des termes de recherche. * Zone de saisie des termes de recherche.
* *
@ -62,10 +28,12 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
const [value, setValue] = useState(''); const [value, setValue] = useState('');
// Liste des termes suggérés par autocomplétion // Liste des termes suggérés par autocomplétion
const [suggestions, setSuggestions] = useState([]); const {
list: suggestions,
// Terme ayant le focus parmi les suggestions setList: setSuggestions,
const [focusedSuggestion, setFocusedSuggestion] = useState(0); focus: focusedSuggestion,
setFocus: setFocusedSuggestion
} = util.useFocusableList();
// Référence au champ de saisie // Référence au champ de saisie
const inputRef = useRef(null); const inputRef = useRef(null);
@ -86,7 +54,6 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
setTerms(terms.concat([term])); setTerms(terms.concat([term]));
setValue(''); setValue('');
setSuggestions([]); setSuggestions([]);
setFocusedSuggestion(0);
if (inputRef.current !== null) if (inputRef.current !== null)
{ {
@ -135,16 +102,13 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
ev.preventDefault(); ev.preventDefault();
setTerms(terms.slice(0, -1)); setTerms(terms.slice(0, -1));
} }
else if (ev.keyCode === keys.up && focusedSuggestion > 0) else if (ev.keyCode === keys.up)
{ {
// Touche Haut : focus de la suggestion précédente // Touche Haut : focus de la suggestion précédente
ev.preventDefault(); ev.preventDefault();
setFocusedSuggestion(focusedSuggestion - 1); setFocusedSuggestion(focusedSuggestion - 1);
} }
else if ( else if (ev.keyCode === keys.down)
ev.keyCode === keys.down
&& focusedSuggestion < suggestions.length - 1
)
{ {
// Touche Bas : focus de la suggestion suivante // Touche Bas : focus de la suggestion suivante
ev.preventDefault(); ev.preventDefault();
@ -169,16 +133,17 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
} }
else else
{ {
setSuggestions(availableTerms setSuggestions(fuzzy.filterTerms(
// Filtre les termes correspondant à la saisie
nextValue,
// Sélectionne les termes qui nont pas déjà été choisis // Sélectionne les termes qui nont pas déjà été choisis
.filter(({id: suggestionId}) => availableTerms.filter(({id: suggestionId}) =>
!terms.some(({id: termId}) => !terms.some(({id: termId}) =>
termId === suggestionId termId === suggestionId
) )
) )
// Filtre ceux dont le nom correspond à la saisie ));
.filter(termMatchesPrefix.bind(null, nextValue))
);
} }
}; };

77
app/src/data/fuzzy.js Normal file
View File

@ -0,0 +1,77 @@
import * as diacritics from 'diacritics';
/**
* Motifs de désuffixation des mots français.
*/
const stemPatterns = [
[/aux$/, 'al'],
[/(.)s$/, '$1'],
[/(.)e$/, '$1'],
];
/**
* Motif de séparation des mots.
*/
const splitPattern = /[\s-]+/g;
/**
* Désuffixe un mot français.
*
* @param word Mot originel.
* @return Mot désuffixé.
*/
const stemWord = word =>
{
let result = word;
for (let [pattern, replacement] of stemPatterns)
{
result = result.replace(pattern, replacement);
}
return result;
};
/**
* Met le nom dun terme sous forme normalisée pour la recherche approximative.
*
* Dans la forme normalisée, tous les accents sont ôtés, la casse est réduite
* en minuscules et les mots sont désuffixés.
*
* @param name Nom à normaliser.
* @return Liste des mots du nom de terme normalisé.
*/
const normalizeName = name =>
diacritics.remove(name.toLowerCase())
.split(splitPattern)
.map(stemWord);
/**
* Filtre une liste de termes pour ne garder que ceux qui sont similaires à une
* saisie.
*
* @param search Contenu de la saisie.
* @param term Terme à vérifier.
* @return Ensemble des termes sélectionnés.
*/
export const filterTerms = (search, terms) =>
{
// Normalisation des mots recherchés
const needle = normalizeName(search);
console.log(needle);
return terms.filter(term =>
{
// Normalisation du nom et des alias du terme
const haystack = [term.name].concat(term.alias)
.map(normalizeName)
.reduce((prev, next) => prev.concat(next), []);
return needle.every(item =>
haystack.some(word =>
word.startsWith(item)
)
);
});
};

View File

@ -24,3 +24,46 @@ export const useAsync = (initial, func, ...args) =>
return results; return results;
}; };
/**
* Crée un état composé dune liste et dun élément ayant le focus dans cette
* liste. À la modification de la liste ou de lindice de lélément ayant le
* focus, la contrainte focus [0, taille de la liste] est imposée.
*
* @return Objet contenant la liste, lindice de lélément ayant le focus ainsi
* que des fonctions de modification.
*/
export const useFocusableList = () =>
{
const [list, setList] = useState([]);
const [focus, setFocus] = useState(0);
return {
list,
focus,
/**
* Modifie la liste et sassure que lélément ayant le focus est
* toujours dans sa plage de valeurs possibles.
*
* @param nextList Nouvelle liste.
*/
setList(nextList)
{
setList(nextList);
setFocus(Math.min(nextList.length, focus));
},
/**
* Modifie lélément ayant le focus en imposant les contraintes
* dintégrité.
*
* @param nextFocus Indice du nouvel élément ayant le focus.
*/
setFocus(nextFocus)
{
setFocus(Math.min(nextList.length, Math.max(0, focus)));
}
};
};