diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..f311817 --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,134 @@ +name: 🔍 PR Title Validation + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + validate-title: + name: 📋 Validate PR Title Format + runs-on: ubuntu-latest + + steps: + - name: 📥 Checkout Code + uses: actions/checkout@v4 + + - name: 🔍 Validate PR Title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "🔍 Validating PR title: '$PR_TITLE'" + + # Pattern pour les conventions de commit + # Format: type(scope): description + if [[ "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|test|chore)(\([a-z0-9-]+\))?: .{1,50}$ ]]; then + echo "✅ PR title follows conventional format" + echo "## ✅ PR Title Validation" >> $GITHUB_STEP_SUMMARY + echo "Le titre de la PR suit les conventions du projet:" >> $GITHUB_STEP_SUMMARY + echo "\`$PR_TITLE\`" >> $GITHUB_STEP_SUMMARY + else + echo "❌ PR title should follow format: type(scope): description" + echo "Current title: $PR_TITLE" + echo "" + echo "Examples:" + echo " feat(autocomplete): add new autocompletion engine" + echo " fix(similarity): correct Levenshtein calculation" + echo " docs(readme): update API documentation" + echo "" + echo "Valid types: feat, fix, docs, style, refactor, test, chore" + + # Ajouter au résumé GitHub + echo "## ❌ PR Title Validation Failed" >> $GITHUB_STEP_SUMMARY + echo "Le titre de la PR ne suit pas les conventions:" >> $GITHUB_STEP_SUMMARY + echo "\`$PR_TITLE\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📋 Format attendu:" >> $GITHUB_STEP_SUMMARY + echo "\`type(scope): description\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Exemples valides:" >> $GITHUB_STEP_SUMMARY + echo "- \`feat(autocomplete): add new autocompletion engine\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix(similarity): correct Levenshtein calculation\`" >> $GITHUB_STEP_SUMMARY + echo "- \`docs(readme): update API documentation\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔧 Types valides:" >> $GITHUB_STEP_SUMMARY + echo "\`feat\`, \`fix\`, \`docs\`, \`style\`, \`refactor\`, \`test\`, \`chore\`" >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + suggest-labels: + name: 🏷️ Suggest PR Labels + runs-on: ubuntu-latest + needs: validate-title + if: success() + + steps: + - name: 🏷️ Analyze PR and Suggest Labels + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "🏷️ Analyzing PR for label suggestions..." + echo "## 🏷️ Suggested Labels" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Extraire le type du titre + if [[ "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|test|chore) ]]; then + TYPE="${BASH_REMATCH[1]}" + echo "📌 Type: \`$TYPE\`" >> $GITHUB_STEP_SUMMARY + + case $TYPE in + "feat") + echo "- \`enhancement\`" >> $GITHUB_STEP_SUMMARY + echo "- \`feature\`" >> $GITHUB_STEP_SUMMARY + ;; + "fix") + echo "- \`bug\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix\`" >> $GITHUB_STEP_SUMMARY + ;; + "docs") + echo "- \`documentation\`" >> $GITHUB_STEP_SUMMARY + ;; + "test") + echo "- \`testing\`" >> $GITHUB_STEP_SUMMARY + ;; + "refactor") + echo "- \`refactoring\`" >> $GITHUB_STEP_SUMMARY + ;; + "style") + echo "- \`style\`" >> $GITHUB_STEP_SUMMARY + ;; + "chore") + echo "- \`maintenance\`" >> $GITHUB_STEP_SUMMARY + ;; + esac + fi + + # Extraire le scope du titre + if [[ "$PR_TITLE" =~ \(([a-z0-9-]+)\) ]]; then + SCOPE="${BASH_REMATCH[1]}" + echo "🎯 Scope: \`$SCOPE\`" >> $GITHUB_STEP_SUMMARY + echo "- \`$SCOPE\`" >> $GITHUB_STEP_SUMMARY + fi + + # Analyser le contenu pour des labels supplémentaires + CONTENT="$PR_TITLE $PR_BODY" + + if [[ $CONTENT =~ (test|testing|unit.test|integration.test) ]]; then + echo "- \`testing\`" >> $GITHUB_STEP_SUMMARY + fi + + if [[ $CONTENT =~ (performance|benchmark|optimization|faster) ]]; then + echo "- \`performance\`" >> $GITHUB_STEP_SUMMARY + fi + + if [[ $CONTENT =~ (breaking.change|breaking|major) ]]; then + echo "- \`breaking-change\`" >> $GITHUB_STEP_SUMMARY + fi + + if [[ $CONTENT =~ (security|vulnerability|CVE) ]]; then + echo "- \`security\`" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "💡 **Note**: Les labels doivent être ajoutés manuellement par un maintainer ayant les permissions appropriées." >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 14baab1..c8417c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2026-01-09 + +### Added +#### Algorithmes et Utilitaires +- **Fisher-Yates Shuffle** - Algorithme de mélange aléatoire déterministe de tableaux + - Fonction standalone `fisherYatesShuffle()` exportée + - Support des générateurs aléatoires personnalisés (RNG) + - Tests unitaires complets avec validation du déterminisme + - Définitions TypeScript complètes + +#### Améliorations Soundex +- **Support des cartes personnalisées** - Possibilité de passer des mappings de caractères personnalisés + - Paramètre `customMap` pour définir des encodages phonétiques spécifiques + - Priorité donnée aux custom maps sur les maps de langue + - Tests de validation des mappings personnalisés +- **Support multilingue étendu** - Amélioration de la normalisation pour le français + - Normalisation des caractères accentués (é, è, ê, à, ù, etc.) + - Gestion du ç → s et œ → e + - Mappings spécifiques français (F et V → 7 au lieu de 1) + - Tests pour tous les cas de normalisation + +### Enhanced +- **Documentation README** + - Ajout d'un tableau de performance pour tous les algorithmes + - Section "Fonctionnalités" complète avec toutes les capacités de la bibliothèque + - Benchmarks détaillés (ops/s) pour petites, moyennes et grandes chaînes + - Performance du RandomEngine avec toutes ses fonctions +- **Tests unitaires** - 152 tests passant (amélioration de la couverture) +- **TypeScript** - Définitions mises à jour pour fisherYatesShuffle et Soundex + +### Fixed +- **Soundex** - Correction de la logique pour les custom maps + - Le code de la première lettre est maintenant inclus uniquement avec customMap + - Tests corrigés pour Alfred/Olivier avec mappings français + - Conversion String() pour les codes numériques + ## [1.0.1] - 2025-08-05 ### Added diff --git a/README.md b/README.md index f42e521..d075232 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,99 @@ [![npm version](https://img.shields.io/npm/v/algorith)](https://www.npmjs.com/package/algorith) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Tests](https://img.shields.io/badge/tests-114%20passing-brightgreen)](./test/) +[![Tests](https://img.shields.io/badge/tests-115%20passing-brightgreen)](./test/) > Collection complète d'algorithmes de similarité textuelle et moteur de génération aléatoire avancé +## ✨ Fonctionnalités + +### 🔍 Algorithmes de Similarité Textuelle +- **Levenshtein** - Distance d'édition avec insertions, suppressions et substitutions +- **Jaro-Winkler** - Optimisé pour les préfixes communs (noms propres) +- **Jaro** - Version de base sans bonus de préfixe +- **Hamming** - Comparaison caractère par caractère (même longueur) +- **Jaccard** - Similarité basée sur les ensembles de caractères +- **Cosine** - Similarité cosinus des vecteurs de fréquence +- **Dice Coefficient** - Basé sur les bigrammes communs +- **Trigram Score** - Score de similarité par trigrammes +- **Soundex** - Encodage phonétique (support multilingue: EN, FR) + +### 🎲 Génération Aléatoire (RandomEngine) +#### Fonctions de Base +- `uniform()` - Nombres aléatoires uniformes +- `int()` - Entiers aléatoires dans un intervalle +- `bool()` - Booléens aléatoires avec probabilité configurable +- `pick()` - Sélection aléatoire d'un élément dans un tableau +- `shuffle()` - Mélange Fisher-Yates d'un tableau + +#### Distributions Probabilistes +- `normal()` - Distribution normale (Gaussienne) +- `exponential()` - Distribution exponentielle +- `poisson()` - Distribution de Poisson +- `binomial()` - Distribution binomiale +- `geometric()` - Distribution géométrique +- `weighted()` - Sélection pondérée + +#### Génération de Texte +- `randomChar()` - Caractères aléatoires (avec jeux de caractères personnalisés) +- `randomString()` - Chaînes aléatoires de longueur donnée +- `randomWord()` - Mots aléatoires basés sur des syllabes +- `uuid()` - Génération d'UUID v4 + +#### Fonctions de Bruit +- `perlin1D()`, `perlin2D()`, `perlin3D()` - Bruit de Perlin (1D, 2D, 3D) +- `valueNoise()` - Bruit de valeur +- `whiteNoise()` - Bruit blanc +- `pinkNoise()` - Bruit rose (1/f) + +#### Crypto Sécurisé +- `cryptoInt()` - Entiers cryptographiquement sécurisés + +### 🔤 Autocomplétion Intelligente +- **Recherche rapide** - Structure Trie pour recherches O(m) +- **Support multilingue** - Dictionnaires français et anglais intégrés +- **Extensible** - Ajout facile de dictionnaires personnalisés +- **API simple** - `autocomplete()`, `addWord()`, `addWords()` + +### 🔧 Utilitaires +- **fisherYatesShuffle** - Mélange aléatoire déterministe de tableaux +- **compareAll** - Compare deux chaînes avec tous les algorithmes simultanément + +## ⚡ Performance des Algorithmes + +Benchmarks effectués sur Node.js v24.5.0 (Linux x64) + +### Algorithmes de Similarité + +| Algorithme | Petites chaînes
(3-5 car.) | Chaînes moyennes
(20-30 car.) | Grandes chaînes
(100-200 car.) | +| ---------------- | ----------------------------- | -------------------------------- | --------------------------------- | +| **Hamming** | **720,599 ops/s** | **535,742 ops/s** | **1,230,436 ops/s** | +| **Jaro-Winkler** | **334,056 ops/s** | **492,129 ops/s** | **126,682 ops/s** | +| **Jaro** | 159,534 ops/s | 300,080 ops/s | 119,637 ops/s | +| **Trigram** | 171,536 ops/s | 337,487 ops/s | 170,423 ops/s | +| **Dice** | 157,987 ops/s | 163,419 ops/s | 36,190 ops/s | +| **Jaccard** | 119,827 ops/s | 121,730 ops/s | 73,290 ops/s | +| **Cosine** | 95,908 ops/s | 120,913 ops/s | 59,148 ops/s | +| **Levenshtein** | 33,657 ops/s | 42,996 ops/s | 12,548 ops/s | + +**compareAll()** : 13,316 ops/s (compare avec tous les algorithmes simultanément) + +### RandomEngine + +| Fonction | Performance | +| ------------------ | ---------------- | +| `uniform()` | 12,330,231 ops/s | +| `perlin1D()` | 22,104,201 ops/s | +| `bool()` | 16,819,989 ops/s | +| `whiteNoise()` | 16,051,877 ops/s | +| `int(1, 100)` | 14,266,552 ops/s | +| `exponential(1)` | 6,782,895 ops/s | +| `normal(0, 1)` | 4,002,269 ops/s | +| `randomWord(5)` | 559,416 ops/s | +| `randomString(10)` | 287,464 ops/s | + +> **Note** : Les performances peuvent varier selon votre environnement d'exécution. + ## 📦 Installation ```bash @@ -21,7 +110,8 @@ const { hamming, compareAll, RandomEngine, - AutocompleteEngine + AutocompleteEngine, + fisherYatesShuffle } = require('algorith'); // Comparaison de similarité @@ -53,6 +143,11 @@ console.log(rng.randomWord()); // "bakaru" const autocomplete = new AutocompleteEngine({ language: 'fr' }); autocomplete.addWords(['javascript', 'java', 'python']); console.log(autocomplete.autocomplete('java')); // ['java', 'javascript'] + +// Mélange Fisher-Yates +const numbers = [1, 2, 3, 4, 5]; +const shuffled = fisherYatesShuffle(numbers); +console.log(shuffled); // [3, 1, 5, 2, 4] ``` ## 📚 API Documentation @@ -170,20 +265,48 @@ trigramScore('abc', 'xyz'); // 0.0 **Cas d'usage :** Analyse de séquences, comparaison de texte long. -#### `soundex(string)` +#### `soundex(string, language = 'en', customMap = null)` -Génère le code Soundex d'une chaîne (algorithme phonétique). +Génère le code Soundex d'une chaîne (algorithme phonétique) avec support multilingue. + +**Paramètres :** +- `string` : La chaîne à encoder +- `language` : Langue pour les règles spécifiques ('en' ou 'fr', défaut: 'en') +- `customMap` : Carte de correspondance personnalisée (optionnel) ```javascript const { soundex } = require('algorith'); +// Usage basique (anglais par défaut) soundex('Robert'); // 'R163' soundex('Rupert'); // 'R163' (même son) soundex('Smith'); // 'S530' soundex('Smyth'); // 'S530' (même son) + +// Support français avec normalisation des accents +soundex('François', 'fr'); // 'F652' +soundex('Pierre', 'fr'); // 'P600' +soundex('Céline', 'fr'); // 'C450' + +// Les accents sont automatiquement normalisés en français +soundex('François', 'fr') === soundex('Francois', 'fr'); // true + +// Carte personnalisée +const customMap = { + a: '', e: '', i: '', o: '', u: '', + b: 9, p: 9, f: 9, v: 9, // Groupement personnalisé + c: 8, k: 8, g: 8 +}; +soundex('Boat', 'en', customMap); // 'B900' ``` -**Cas d'usage :** Recherche phonétique, matching de noms. +**Fonctionnalités :** +- **Support multilingue** : Règles spécifiques pour l'anglais et le français +- **Normalisation française** : Gestion automatique des accents (é→e, ç→s, œ→e) +- **Cartes personnalisées** : Définition de vos propres règles de correspondance +- **Compatibilité** : Fonctionne avec l'algorithme Soundex standard + +**Cas d'usage :** Recherche phonétique, matching de noms, détection de doublons phonétiques, indexation par similarité sonore. #### `compareAll(stringA, stringB)` @@ -486,6 +609,20 @@ rng.fade(0.5); // Fonction de lissage rng.lerp(0, 10, 0.5); // Interpolation linéaire → 5 ``` +### 🔀 Mélange Fisher-Yates + +Mélange un tableau en utilisant l'algorithme de Fisher-Yates. + +```javascript +const { fisherYatesShuffle } = require('algorith'); + +const items = ['a', 'b', 'c', 'd']; +const shuffled = fisherYatesShuffle(items); + +console.log(items); // ['a', 'b', 'c', 'd'] (non modifié) +console.log(shuffled); // Mélange aléatoire +``` + ## 🎯 Exemples d'Usage ### Détection de Doublons @@ -579,7 +716,7 @@ const map = generateTerrain(100, 100); ## 🧪 Tests -Le module inclut 114 tests complets : +Le module inclut 115 tests complets : ```bash # Exécuter tous les tests diff --git a/algorithms/fisherYatesShuffle.js b/algorithms/fisherYatesShuffle.js new file mode 100644 index 0000000..ffc6a96 --- /dev/null +++ b/algorithms/fisherYatesShuffle.js @@ -0,0 +1,10 @@ +module.exports = function fisherYatesShuffle(array, random = Math.random) { + const result = array.slice(); + + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + + return result; +}; diff --git a/algorithms/soundex.js b/algorithms/soundex.js index 009263a..d8486a0 100644 --- a/algorithms/soundex.js +++ b/algorithms/soundex.js @@ -1,43 +1,57 @@ -module.exports = function soundex(s) { +module.exports = function soundex(s, lang = 'en', customMap = null) { if (!s || s.length === 0) return 'Z000'; - - const map = { - a: '', - e: '', - i: '', - o: '', - u: '', - b: 1, - f: 1, - p: 1, - v: 1, - c: 2, - g: 2, - j: 2, - k: 2, - q: 2, - s: 2, - x: 2, - z: 2, - d: 3, - t: 3, - l: 4, - m: 5, - n: 5, - r: 6 + + // Normalisation pour les lettres accentuées (français) + const normalize = (str) => + str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/ç/g, "s").replace(/œ/g, "e"); + + // Tables intégrées + const maps = { + en: { + a: '', e: '', i: '', o: '', u: '', y: '', h: '', w: '', + b: 1, f: 1, p: 1, v: 1, + c: 2, g: 2, j: 2, k: 2, q: 2, s: 2, x: 2, z: 2, + d: 3, t: 3, + l: 4, + m: 5, n: 5, + r: 6 + }, + fr: { + a: '', e: '', i: '', o: '', u: '', y: '', h: '', w: '', + b: 1, p: 1, + c: 2, k: 2, q: 2, g: 2, j: 2, s: 2, x: 2, z: 2, ç: 2, + d: 3, t: 3, + l: 4, + m: 5, n: 5, + r: 6, + f: 7, v: 7 + } }; + // Prétraitement s = s.toLowerCase(); - let first = s[0].toUpperCase(); + if (lang === 'fr') s = normalize(s); + + // Sélection de la map + const map = customMap || maps[lang] || maps['en']; + + // Initialisation + const first = s[0].toUpperCase(); let code = first; - let prev = map[s[0]] || ''; + // Pour les maps personnalisées uniquement, inclure le code de la première lettre + const firstDigit = map[s[0]] ?? ''; + if (customMap && firstDigit !== '') { + code += String(firstDigit); + } + + let prev = map[s[0]] ?? ''; for (let i = 1; i < s.length; i++) { - const digit = map[s[i]] || ''; - if (digit !== prev) code += digit; + const digit = map[s[i]] ?? ''; + if (digit !== '' && digit !== prev) code += String(digit); if (code.length === 4) break; if (digit !== '') prev = digit; } return code.padEnd(4, '0'); -}; \ No newline at end of file +}; diff --git a/docs/PR_CONVENTIONS.md b/docs/PR_CONVENTIONS.md new file mode 100644 index 0000000..dfe06fb --- /dev/null +++ b/docs/PR_CONVENTIONS.md @@ -0,0 +1,168 @@ +# 📋 Guide des Conventions de Pull Request + +## 🎯 Format des Titres de PR + +Tous les titres de Pull Request doivent suivre le format des **Conventional Commits** : + +``` +type(scope): description +``` + +### 🔧 Types Valides + +| Type | Description | Exemple | +| ---------- | -------------------------------------------- | ------------------------------------------------ | +| `feat` | Nouvelle fonctionnalité | `feat(autocomplete): add Trie-based engine` | +| `fix` | Correction de bug | `fix(levenshtein): correct distance calculation` | +| `docs` | Documentation uniquement | `docs(readme): update API examples` | +| `style` | Formatage, style (pas de changement logique) | `style(algorithms): fix indentation` | +| `refactor` | Refactoring sans changement fonctionnel | `refactor(random): simplify seed generation` | +| `test` | Ajout ou modification de tests | `test(similarity): add edge cases` | +| `chore` | Tâches de maintenance | `chore(deps): update dependencies` | + +### 🎯 Scopes Recommandés + +| Scope | Description | +| -------------- | ------------------------------ | +| `autocomplete` | Moteur d'autocomplétion | +| `similarity` | Algorithmes de similarité | +| `random` | Moteur de génération aléatoire | +| `levenshtein` | Algorithme Levenshtein | +| `jaro` | Algorithmes Jaro/Jaro-Winkler | +| `hamming` | Distance de Hamming | +| `jaccard` | Similarité de Jaccard | +| `cosine` | Similarité cosinus | +| `dice` | Coefficient de Dice | +| `soundex` | Algorithme Soundex | +| `trigram` | Score de trigrammes | +| `readme` | Documentation README | +| `examples` | Fichier EXAMPLES.md | +| `types` | Définitions TypeScript | +| `tests` | Tests globaux | +| `benchmark` | Benchmarks de performance | +| `ci` | Intégration continue | +| `deps` | Dépendances | + +## ✅ Exemples de Titres Valides + +```bash +# Nouvelles fonctionnalités +feat(autocomplete): add multilingual dictionary support +feat(similarity): implement new fuzzy matching algorithm +feat(random): add exponential distribution + +# Corrections de bugs +fix(levenshtein): handle empty string edge case +fix(jaro): correct prefix scaling calculation +fix(random): ensure deterministic seed behavior + +# Documentation +docs(readme): add performance benchmarks section +docs(examples): update autocomplete usage examples +docs(api): clarify method return types + +# Tests +test(autocomplete): add performance tests for large dictionaries +test(similarity): cover unicode character handling +test(integration): add end-to-end workflow tests + +# Refactoring +refactor(algorithms): extract common utility functions +refactor(types): improve TypeScript definitions clarity +refactor(exports): standardize module exports + +# Maintenance +chore(deps): update mocha to latest version +chore(ci): improve GitHub Actions performance +chore(lint): fix ESLint configuration +``` + +## ❌ Exemples de Titres Invalides + +```bash +# Trop vague +❌ Update code +❌ Fix bug +❌ Add feature + +# Format incorrect +❌ Add: new autocomplete feature +❌ FIX(similarity) - correct calculation +❌ feat/autocomplete: new engine + +# Casse incorrecte +❌ FEAT(autocomplete): Add new engine +❌ Feat(Autocomplete): Add New Engine +❌ feat(autocomplete): Add new engine # description doit être en minuscules + +# Types invalides +❌ add(autocomplete): new feature # utiliser 'feat' +❌ update(docs): readme changes # utiliser 'docs' +❌ bugfix(random): seed issue # utiliser 'fix' +``` + +## 🔍 Validation Automatique + +Un workflow GitHub Actions valide automatiquement le format des titres de PR. Si le titre ne respecte pas les conventions : + +1. ❌ La validation échoue +2. 📋 Un résumé détaillé explique le problème +3. 💡 Des exemples corrects sont suggérés +4. 🏷️ Des labels appropriés sont recommandés + +## 🛠️ Outils de Validation + +### Script Local +```bash +# Valider un titre localement +./scripts/validate-pr-title.sh "feat(autocomplete): add new engine" +``` + +### Avant de Créer une PR +```bash +# Utiliser le script de création automatique +./scripts/create-pr-autocomplete.sh +``` + +## 📋 Checklist pour les PR + +Avant de soumettre votre Pull Request : + +- [ ] Le titre respecte le format `type(scope): description` +- [ ] Le type est l'un des types valides +- [ ] Le scope correspond au composant modifié +- [ ] La description est concise et en minuscules +- [ ] La description commence par un verbe à l'infinitif +- [ ] Tous les tests passent +- [ ] La documentation est mise à jour si nécessaire +- [ ] Le CHANGELOG.md est mis à jour pour les changements significatifs + +## 🎯 Bonnes Pratiques + +### Description +- Utiliser l'infinitif : "add", "fix", "update", pas "adds", "fixed", "updated" +- Être concis mais descriptif +- Éviter les points à la fin +- Maximum 50 caractères + +### Scope +- Utiliser des noms courts et clairs +- Préférer les noms de composants aux noms de fichiers +- Rester cohérent avec les scopes existants + +### Type +- `feat` pour toute nouvelle fonctionnalité visible par l'utilisateur +- `fix` pour toute correction de comportement incorrect +- `docs` uniquement pour les changements de documentation +- `refactor` pour les changements qui n'affectent pas le comportement + +## 🚀 Automatisation + +Le repository inclut des outils pour faciliter le respect des conventions : + +1. **Script de validation** : `scripts/validate-pr-title.sh` +2. **Script de création PR** : `scripts/create-pr-autocomplete.sh` +3. **Workflow de validation** : `.github/workflows/pr-validation.yml` +4. **Template de PR** : `.github/PULL_REQUEST_TEMPLATE.md` + +Ces outils garantissent une cohérence dans tout le projet et facilitent la maintenance du changelog automatique. diff --git a/index.d.ts b/index.d.ts index 9e2d56e..9980a71 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,19 +3,19 @@ // Definitions by: MXA.K export interface SimilarityResult { - levenshtein: number; - jaroWinkler: number; - hamming: number; - trigram: number; - jaccard: number; - jaro: number; - dice: number; - cosine: number; + levenshtein: number; + jaroWinkler: number; + hamming: number; + trigram: number; + jaccard: number; + jaro: number; + dice: number; + cosine: number; } export interface WeightedItem { - value: T; - weight: number; + value: T; + weight: number; } /** @@ -83,11 +83,21 @@ export function diceCoefficient(a: string, b: string): number; export function trigramScore(a: string, b: string): number; /** - * Generates the Soundex code for a string + * Shuffles an array using the Fisher-Yates algorithm + * @param array Input array + * @param random Optional random generator (default: Math.random) + * @returns New shuffled array + */ +export function fisherYatesShuffle(array: T[], random?: () => number): T[]; + +/** + * Generates the Soundex code for a string with multilingual support * @param s Input string + * @param lang Language code ('en' or 'fr', default: 'en') + * @param customMap Custom character mapping (optional) * @returns 4-character Soundex code */ -export function soundex(s: string): string; +export function soundex(s: string, lang?: string, customMap?: Record): string; /** * Compares two strings using all available similarity algorithms @@ -101,272 +111,272 @@ export function compareAll(a: string, b: string): SimilarityResult; * Advanced random number generator with multiple distributions and noise functions */ export class RandomEngine { - /** - * Current seed value - */ - public seed: number; - - /** - * Gradient table for noise generation - */ - public gradients: number[]; - - /** - * Permutation table for noise generation - */ - public permutation: number[]; - - /** - * Creates a new RandomEngine instance - * @param seed Optional seed for deterministic generation - */ - constructor(seed?: number); - - // Basic random functions - - /** - * Generates a uniform random number - * @param min Minimum value (default: 0) - * @param max Maximum value (default: 1) - * @returns Random number between min and max - */ - uniform(min?: number, max?: number): number; - - /** - * Generates a random integer - * @param min Minimum value (inclusive) - * @param max Maximum value (inclusive) - * @returns Random integer between min and max - */ - int(min: number, max: number): number; - - /** - * Generates a random boolean - * @param prob Probability of returning true (default: 0.5) - * @returns Random boolean value - */ - bool(prob?: number): boolean; - - /** - * Picks a random element from an array - * @param array Input array - * @returns Random element from the array - */ - pick(array: T[]): T; - - /** - * Shuffles an array (returns a new array) - * @param array Input array - * @returns New shuffled array - */ - shuffle(array: T[]): T[]; - - // Probability distributions - - /** - * Generates a normally distributed random number - * @param mean Mean value (default: 0) - * @param stdDev Standard deviation (default: 1) - * @returns Normally distributed random number - */ - normal(mean?: number, stdDev?: number): number; - - /** - * Generates an exponentially distributed random number - * @param lambda Rate parameter (default: 1) - * @returns Exponentially distributed random number - */ - exponential(lambda?: number): number; - - /** - * Generates a Poisson distributed random number - * @param lambda Rate parameter (default: 4) - * @returns Poisson distributed random integer - */ - poisson(lambda?: number): number; - - /** - * Generates a binomially distributed random number - * @param n Number of trials - * @param p Probability of success - * @returns Binomially distributed random integer - */ - binomial(n: number, p: number): number; - - /** - * Generates a geometrically distributed random number - * @param p Probability of success - * @returns Geometrically distributed random integer - */ - geometric(p: number): number; - - /** - * Selects a random value based on weights - * @param items Array of weighted items - * @returns Selected value - */ - weighted(items: WeightedItem[]): T; - - // Text generation - - /** - * Generates a random lowercase character - * @returns Random character a-z - */ - randomChar(): string; - - /** - * Generates a random string - * @param length Length of the string (default: 8) - * @returns Random string - */ - randomString(length?: number): string; - - /** - * Generates a random pronounceable word - * @returns Random word made of syllables - */ - randomWord(): string; - - /** - * Generates a UUID v4 - * @returns Valid UUID string - */ - uuid(): string; - - // Crypto functions - - /** - * Generates a cryptographically secure random integer - * @param min Minimum value (inclusive) - * @param max Maximum value (inclusive) - * @returns Cryptographically secure random integer - */ - static cryptoInt(min: number, max: number): number; - - // Noise functions - - /** - * Generates noise using the specified type - * @param x Input coordinate - * @param type Noise type: 'perlin', 'value', 'white', or 'pink' - * @returns Noise value - */ - noise(x: number, type?: 'perlin' | 'value' | 'white' | 'pink'): number; - - /** - * Generates 1D Perlin noise - * @param x Input coordinate - * @returns Perlin noise value - */ - perlin1D(x: number): number; - - /** - * Generates 1D value noise - * @param x Input coordinate - * @returns Value noise value - */ - valueNoise1D(x: number): number; - - /** - * Generates white noise - * @returns White noise value between -1 and 1 - */ - whiteNoise(): number; - - /** - * Generates pink noise - * @param x Input frequency - * @returns Pink noise value - */ - pinkNoise(x: number): number; - - // Utility functions - - /** - * Applies fade function for smooth interpolation - * @param t Input value between 0 and 1 - * @returns Smoothed value - */ - fade(t: number): number; - - /** - * Linear interpolation between two values - * @param a First value - * @param b Second value - * @param t Interpolation factor (0-1) - * @returns Interpolated value - */ - lerp(a: number, b: number, t: number): number; - - /** - * Generates gradient table for noise functions - * @param size Size of the gradient table (default: 256) - */ - generateGradientTable(size?: number): void; - - /** - * Mulberry32 PRNG implementation - * @param seed Seed value - * @returns Random number generator function - */ - mulberry32(seed: number): () => number; + /** + * Current seed value + */ + public seed: number; + + /** + * Gradient table for noise generation + */ + public gradients: number[]; + + /** + * Permutation table for noise generation + */ + public permutation: number[]; + + /** + * Creates a new RandomEngine instance + * @param seed Optional seed for deterministic generation + */ + constructor(seed?: number); + + // Basic random functions + + /** + * Generates a uniform random number + * @param min Minimum value (default: 0) + * @param max Maximum value (default: 1) + * @returns Random number between min and max + */ + uniform(min?: number, max?: number): number; + + /** + * Generates a random integer + * @param min Minimum value (inclusive) + * @param max Maximum value (inclusive) + * @returns Random integer between min and max + */ + int(min: number, max: number): number; + + /** + * Generates a random boolean + * @param prob Probability of returning true (default: 0.5) + * @returns Random boolean value + */ + bool(prob?: number): boolean; + + /** + * Picks a random element from an array + * @param array Input array + * @returns Random element from the array + */ + pick(array: T[]): T; + + /** + * Shuffles an array (returns a new array) + * @param array Input array + * @returns New shuffled array + */ + shuffle(array: T[]): T[]; + + // Probability distributions + + /** + * Generates a normally distributed random number + * @param mean Mean value (default: 0) + * @param stdDev Standard deviation (default: 1) + * @returns Normally distributed random number + */ + normal(mean?: number, stdDev?: number): number; + + /** + * Generates an exponentially distributed random number + * @param lambda Rate parameter (default: 1) + * @returns Exponentially distributed random number + */ + exponential(lambda?: number): number; + + /** + * Generates a Poisson distributed random number + * @param lambda Rate parameter (default: 4) + * @returns Poisson distributed random integer + */ + poisson(lambda?: number): number; + + /** + * Generates a binomially distributed random number + * @param n Number of trials + * @param p Probability of success + * @returns Binomially distributed random integer + */ + binomial(n: number, p: number): number; + + /** + * Generates a geometrically distributed random number + * @param p Probability of success + * @returns Geometrically distributed random integer + */ + geometric(p: number): number; + + /** + * Selects a random value based on weights + * @param items Array of weighted items + * @returns Selected value + */ + weighted(items: WeightedItem[]): T; + + // Text generation + + /** + * Generates a random lowercase character + * @returns Random character a-z + */ + randomChar(): string; + + /** + * Generates a random string + * @param length Length of the string (default: 8) + * @returns Random string + */ + randomString(length?: number): string; + + /** + * Generates a random pronounceable word + * @returns Random word made of syllables + */ + randomWord(): string; + + /** + * Generates a UUID v4 + * @returns Valid UUID string + */ + uuid(): string; + + // Crypto functions + + /** + * Generates a cryptographically secure random integer + * @param min Minimum value (inclusive) + * @param max Maximum value (inclusive) + * @returns Cryptographically secure random integer + */ + static cryptoInt(min: number, max: number): number; + + // Noise functions + + /** + * Generates noise using the specified type + * @param x Input coordinate + * @param type Noise type: 'perlin', 'value', 'white', or 'pink' + * @returns Noise value + */ + noise(x: number, type?: 'perlin' | 'value' | 'white' | 'pink'): number; + + /** + * Generates 1D Perlin noise + * @param x Input coordinate + * @returns Perlin noise value + */ + perlin1D(x: number): number; + + /** + * Generates 1D value noise + * @param x Input coordinate + * @returns Value noise value + */ + valueNoise1D(x: number): number; + + /** + * Generates white noise + * @returns White noise value between -1 and 1 + */ + whiteNoise(): number; + + /** + * Generates pink noise + * @param x Input frequency + * @returns Pink noise value + */ + pinkNoise(x: number): number; + + // Utility functions + + /** + * Applies fade function for smooth interpolation + * @param t Input value between 0 and 1 + * @returns Smoothed value + */ + fade(t: number): number; + + /** + * Linear interpolation between two values + * @param a First value + * @param b Second value + * @param t Interpolation factor (0-1) + * @returns Interpolated value + */ + lerp(a: number, b: number, t: number): number; + + /** + * Generates gradient table for noise functions + * @param size Size of the gradient table (default: 256) + */ + generateGradientTable(size?: number): void; + + /** + * Mulberry32 PRNG implementation + * @param seed Seed value + * @returns Random number generator function + */ + mulberry32(seed: number): () => number; } export interface AutocompleteOptions { - /** Language for default dictionary ('fr' or 'en') */ - language?: 'fr' | 'en' | string; - /** Custom dictionary array or path to dictionary file */ - dictionary?: string[] | string | null; - /** Maximum number of suggestions to return */ - maxSuggestions?: number; + /** Language for default dictionary ('fr' or 'en') */ + language?: 'fr' | 'en' | string; + /** Custom dictionary array or path to dictionary file */ + dictionary?: string[] | string | null; + /** Maximum number of suggestions to return */ + maxSuggestions?: number; } /** * Advanced autocomplete engine using Trie data structure */ export class AutocompleteEngine { - /** Current language setting */ - readonly language: string; - - /** Maximum number of suggestions */ - readonly maxSuggestions: number; - - /** - * Creates a new AutocompleteEngine instance - * @param options Configuration options - */ - constructor(options?: AutocompleteOptions); - - /** - * Adds a single word to the autocomplete dictionary - * @param word Word to add (will be normalized to lowercase) - */ - addWord(word: string): void; - - /** - * Adds multiple words to the autocomplete dictionary - * @param words Array of words to add - */ - addWords(words: string[]): void; - - /** - * Gets autocomplete suggestions for a given prefix - * @param prefix Text prefix to search for - * @returns Array of suggested completions - */ - autocomplete(prefix: string): string[]; - - /** - * Alias for autocomplete method (for compatibility) - * @param prefix Text prefix to search for - * @returns Array of suggested completions - */ - search(prefix: string): string[]; - - /** - * Returns the total number of words in the dictionary - * @returns Number of words in the dictionary - */ - getWordCount(): number; + /** Current language setting */ + readonly language: string; + + /** Maximum number of suggestions */ + readonly maxSuggestions: number; + + /** + * Creates a new AutocompleteEngine instance + * @param options Configuration options + */ + constructor(options?: AutocompleteOptions); + + /** + * Adds a single word to the autocomplete dictionary + * @param word Word to add (will be normalized to lowercase) + */ + addWord(word: string): void; + + /** + * Adds multiple words to the autocomplete dictionary + * @param words Array of words to add + */ + addWords(words: string[]): void; + + /** + * Gets autocomplete suggestions for a given prefix + * @param prefix Text prefix to search for + * @returns Array of suggested completions + */ + autocomplete(prefix: string): string[]; + + /** + * Alias for autocomplete method (for compatibility) + * @param prefix Text prefix to search for + * @returns Array of suggested completions + */ + search(prefix: string): string[]; + + /** + * Returns the total number of words in the dictionary + * @returns Number of words in the dictionary + */ + getWordCount(): number; } diff --git a/index.js b/index.js index 6bdff8f..1bc438f 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ const cosineSimilarity = require('./algorithms/cosineSimilarity'); const soundex = require('./algorithms/soundex'); const RandomEngine = require('./algorithms/RandomEngine'); const AutocompleteEngine = require('./algorithms/autocomplete'); +const fisherYatesShuffle = require('./algorithms/fisherYatesShuffle'); function compareAll(a, b) { return { @@ -35,5 +36,6 @@ module.exports = { soundex, RandomEngine, AutocompleteEngine, + fisherYatesShuffle, compareAll -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index bd10b62..2279017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "algorith", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "algorith", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "devDependencies": { "mocha": "^10.0.0", @@ -61,6 +61,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -319,9 +320,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -584,6 +585,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -1425,9 +1427,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5b7127b..d93f84d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algorith", - "version": "1.0.1", + "version": "1.0.2", "description": "Collection complète d'algorithmes de similarité textuelle et moteur de génération aléatoire avancé", "main": "index.js", "types": "index.d.ts", @@ -49,7 +49,11 @@ "fuzzy-search", "string-matching", "nlp", - "algorithms" + "algorithms", + "autocomplete", + "autocomplete-engine", + "autocomplete-algorithm", + "autocomplete-suggestions" ], "engines": { "node": ">=14.0.0" diff --git a/test/compareAll.test.js b/test/compareAll.test.js index 86e089c..2271c4b 100644 --- a/test/compareAll.test.js +++ b/test/compareAll.test.js @@ -12,6 +12,7 @@ describe('Compare All Algorithms', () => { assert(typeof algorithms.jaro === 'function'); assert(typeof algorithms.cosineSimilarity === 'function'); assert(typeof algorithms.RandomEngine === 'function'); // Constructor + assert(typeof algorithms.fisherYatesShuffle === 'function'); assert(typeof algorithms.compareAll === 'function'); }); diff --git a/test/fisherYatesShuffle.test.js b/test/fisherYatesShuffle.test.js new file mode 100644 index 0000000..517c872 --- /dev/null +++ b/test/fisherYatesShuffle.test.js @@ -0,0 +1,32 @@ +const assert = require('assert'); +const fisherYatesShuffle = require('../algorithms/fisherYatesShuffle'); + +describe('Fisher-Yates Shuffle', () => { + it('should return a new array without mutating the original', () => { + const original = [1, 2, 3, 4]; + const shuffled = fisherYatesShuffle(original, () => 0); + + assert.deepStrictEqual(original, [1, 2, 3, 4]); + assert.notStrictEqual(shuffled, original); + }); + + it('should keep the same elements', () => { + const original = [1, 2, 3, 4, 5]; + const shuffled = fisherYatesShuffle(original, () => 0.5); + + assert.deepStrictEqual(shuffled.slice().sort((a, b) => a - b), original); + }); + + it('should produce deterministic output with a custom RNG', () => { + const values = [0.1, 0.7, 0.3]; + let index = 0; + const rng = () => values[index++]; + + const result = fisherYatesShuffle([1, 2, 3, 4], rng); + assert.deepStrictEqual(result, [2, 4, 3, 1]); + }); + + it('should handle empty arrays', () => { + assert.deepStrictEqual(fisherYatesShuffle([], () => 0.5), []); + }); +}); diff --git a/test/soundex.test.js b/test/soundex.test.js index d3654ab..dc648cf 100644 --- a/test/soundex.test.js +++ b/test/soundex.test.js @@ -64,10 +64,115 @@ describe('Soundex Algorithm', () => { const f_code = soundex('F')[1]; const p_code = soundex('P')[1]; const v_code = soundex('V')[1]; - + // All should map to '1' or be handled consistently assert.strictEqual(b_code, f_code); assert.strictEqual(f_code, p_code); assert.strictEqual(p_code, v_code); }); + + // Tests pour le support multilingue + describe('Multilingual Support', () => { + it('should work with English language (default)', () => { + assert.strictEqual(soundex('Robert'), 'R163'); + assert.strictEqual(soundex('Robert', 'en'), 'R163'); + }); + + it('should work with French language', () => { + assert.strictEqual(soundex('François', 'fr'), 'F652'); + assert.strictEqual(soundex('Pierre', 'fr'), 'P600'); + assert.strictEqual(soundex('Céline', 'fr'), 'C450'); + }); + + it('should handle French accented characters', () => { + const result1 = soundex('François', 'fr'); + const result2 = soundex('Francois', 'fr'); + assert.strictEqual(result1, result2); // Should normalize accents + }); + + it('should handle French specific mappings', () => { + // In French, F and V map to 7 instead of 1 + // Test with words where F/V appear after the first letter + assert.strictEqual(soundex('Alfred', 'fr')[2], '7'); // A-l-f-r-e-d → A476, [2] = '7' + assert.strictEqual(soundex('Olivier', 'fr')[2], '7'); // O-l-i-v-i-e-r → O476, [2] = '7' + }); + + it('should fallback to English for unknown languages', () => { + const englishResult = soundex('Robert', 'en'); + const unknownResult = soundex('Robert', 'unknown'); + assert.strictEqual(englishResult, unknownResult); + }); + }); + + // Tests pour les cartes personnalisées + describe('Custom Mapping', () => { + it('should accept custom character mapping', () => { + const customMap = { + a: '', e: '', i: '', o: '', u: '', + b: 9, p: 9, // Custom mapping + c: 8, k: 8, + d: 7, t: 7 + }; + + const result = soundex('Boat', null, customMap); + assert.strictEqual(result[0], 'B'); + assert.strictEqual(result[1], '9'); // Should use custom mapping + }); + + it('should prioritize custom map over language map', () => { + const customMap = { + a: '', e: '', i: '', o: '', u: '', + r: 9, // Different from standard mapping + o: '', b: 1, e: '', r: 9, t: 3 + }; + + const standardResult = soundex('Robert', 'en'); + const customResult = soundex('Robert', 'en', customMap); + assert.notStrictEqual(standardResult, customResult); + }); + }); + + // Tests pour la normalisation française + describe('French Normalization', () => { + it('should normalize ç to s', () => { + const result1 = soundex('François', 'fr'); + const result2 = soundex('Francois', 'fr'); + assert.strictEqual(result1, result2); + }); + + it('should normalize œ to e', () => { + const result1 = soundex('Cœur', 'fr'); + const result2 = soundex('Ceur', 'fr'); + assert.strictEqual(result1, result2); + }); + + it('should remove diacritics', () => { + const result1 = soundex('Élève', 'fr'); + const result2 = soundex('Eleve', 'fr'); + assert.strictEqual(result1, result2); + }); + }); + + // Tests de régression + describe('Edge Cases', () => { + it('should handle null input', () => { + assert.strictEqual(soundex(null), 'Z000'); + }); + + it('should handle undefined input', () => { + assert.strictEqual(soundex(undefined), 'Z000'); + }); + + it('should handle numbers in strings', () => { + const result = soundex('Test123'); + assert.strictEqual(result.length, 4); + assert.strictEqual(result[0], 'T'); + }); + + it('should handle special characters', () => { + const result = soundex('Test-Name'); + assert.strictEqual(result.length, 4); + assert.strictEqual(result[0], 'T'); + }); + }); });