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 * as diacritics from 'diacritics';
import * as fuzzy from '../data/fuzzy.js';
import * as util from '../util.js';
/**
* Codes des touches du clavier par nom.
@ -13,41 +14,6 @@ const keys = Object.assign(Object.create(null), {
'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.
*
@ -62,10 +28,12 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
const [value, setValue] = useState('');
// Liste des termes suggérés par autocomplétion
const [suggestions, setSuggestions] = useState([]);
// Terme ayant le focus parmi les suggestions
const [focusedSuggestion, setFocusedSuggestion] = useState(0);
const {
list: suggestions,
setList: setSuggestions,
focus: focusedSuggestion,
setFocus: setFocusedSuggestion
} = util.useFocusableList();
// Référence au champ de saisie
const inputRef = useRef(null);
@ -86,7 +54,6 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
setTerms(terms.concat([term]));
setValue('');
setSuggestions([]);
setFocusedSuggestion(0);
if (inputRef.current !== null)
{
@ -135,16 +102,13 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
ev.preventDefault();
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
ev.preventDefault();
setFocusedSuggestion(focusedSuggestion - 1);
}
else if (
ev.keyCode === keys.down
&& focusedSuggestion < suggestions.length - 1
)
else if (ev.keyCode === keys.down)
{
// Touche Bas : focus de la suggestion suivante
ev.preventDefault();
@ -169,16 +133,17 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
}
else
{
setSuggestions(availableTerms
setSuggestions(fuzzy.filterTerms(
// Filtre les termes correspondant à la saisie
nextValue,
// Sélectionne les termes qui nont pas déjà été choisis
.filter(({id: suggestionId}) =>
availableTerms.filter(({id: suggestionId}) =>
!terms.some(({id: termId}) =>
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;
};
/**
* 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)));
}
};
};