107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import type { CodevFiche, CodevMatch } from '~/types/codev'
|
|
|
|
const STOP_WORDS_FR = new Set([
|
|
'le', 'la', 'les', 'un', 'une', 'des', 'de', 'du', 'au', 'aux',
|
|
'et', 'ou', 'mais', 'donc', 'car', 'ni', 'or',
|
|
'a', 'en', 'pour', 'par', 'sur', 'avec', 'sans', 'dans', 'sous',
|
|
'je', 'tu', 'il', 'elle', 'on', 'nous', 'vous', 'ils', 'elles',
|
|
'mon', 'ma', 'mes', 'ton', 'ta', 'tes', 'son', 'sa', 'ses',
|
|
'notre', 'nos', 'votre', 'vos', 'leur', 'leurs',
|
|
'ce', 'cet', 'cette', 'ces', 'qui', 'que', 'quoi', 'dont',
|
|
'est', 'sont', 'etre', 'ai', 'as', 'avoir',
|
|
'pas', 'plus', 'moins', 'tres', 'aussi', 'bien', 'tout', 'tous',
|
|
'me', 'te', 'se', 'lui', 'leur', 'y',
|
|
])
|
|
|
|
function tokenize(text: string): Set<string> {
|
|
if (!text) return new Set()
|
|
const tokens = text
|
|
.toLowerCase()
|
|
.replace(/[.,;:!?()'"\-/]/g, ' ')
|
|
.split(/\s+/)
|
|
.filter((t) => t.length >= 3 && !STOP_WORDS_FR.has(t))
|
|
return new Set(tokens)
|
|
}
|
|
|
|
function jaccard(a: Set<string>, b: Set<string>): number {
|
|
if (a.size === 0 || b.size === 0) return 0
|
|
let inter = 0
|
|
for (const x of a) if (b.has(x)) inter++
|
|
const union = a.size + b.size - inter
|
|
return union === 0 ? 0 : inter / union
|
|
}
|
|
|
|
function score(textA: string, hashtagsA: string[], textB: string, hashtagsB: string[]): number {
|
|
const tagsA = new Set(hashtagsA.map((h) => h.toLowerCase()))
|
|
const tagsB = new Set(hashtagsB.map((h) => h.toLowerCase()))
|
|
|
|
if (tagsA.size > 0 && tagsB.size > 0) {
|
|
return jaccard(tagsA, tagsB)
|
|
}
|
|
return jaccard(tokenize(textA), tokenize(textB))
|
|
}
|
|
|
|
// scoreDirect tokenise TOUJOURS les textes, ignore les hashtags
|
|
// Utilise pour matchSolution : besoin vs offre doivent etre compares par leur contenu reel
|
|
function scoreDirect(textA: string, textB: string): number {
|
|
return jaccard(tokenize(textA), tokenize(textB))
|
|
}
|
|
|
|
export function matchSolution(fiches: CodevFiche[], threshold = 0.18): CodevMatch[] {
|
|
const matches: CodevMatch[] = []
|
|
for (const a of fiches) {
|
|
for (const b of fiches) {
|
|
if (a.id === b.id) continue
|
|
// Solution : on compare le TEXTE besoin de A avec le TEXTE offre de B
|
|
// On ignore les hashtags pour differencier besoin et offre
|
|
const s = scoreDirect(a.besoin, b.offre)
|
|
if (s >= threshold) {
|
|
matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'solution' })
|
|
}
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
export function matchAlliance(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
|
|
const matches: CodevMatch[] = []
|
|
for (let i = 0; i < fiches.length; i++) {
|
|
for (let j = i + 1; j < fiches.length; j++) {
|
|
const a = fiches[i], b = fiches[j]
|
|
// Alliance : besoins similaires — on compare hashtags si presents, sinon textes
|
|
const s = score(a.besoin, a.hashtags, b.besoin, b.hashtags)
|
|
if (s >= threshold) {
|
|
matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'alliance' })
|
|
}
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
export function matchSurprise(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
|
|
const matches: CodevMatch[] = []
|
|
for (let i = 0; i < fiches.length; i++) {
|
|
for (let j = i + 1; j < fiches.length; j++) {
|
|
const a = fiches[i], b = fiches[j]
|
|
// Surprise : offres similaires
|
|
const s = score(a.offre, a.hashtags, b.offre, b.hashtags)
|
|
if (s >= threshold) {
|
|
matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'surprise' })
|
|
}
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
export function computeMatches(
|
|
fiches: CodevFiche[],
|
|
mode: 'solution' | 'alliance' | 'surprise',
|
|
threshold?: number,
|
|
): CodevMatch[] {
|
|
switch (mode) {
|
|
case 'solution': return matchSolution(fiches, threshold)
|
|
case 'alliance': return matchAlliance(fiches, threshold)
|
|
case 'surprise': return matchSurprise(fiches, threshold)
|
|
}
|
|
}
|