enum ContentGroupMatch { NO_MATCH, PARTIAL_MATCH, PERFECT_MATCH } interface ContentGroupMatchResult { type : ContentGroupMatch; unmatched : Array; } class ContentGroup { private units : Array = []; private matching : Array; constructor (...units : Array) { units.forEach(unit => { this.addUnit(unit); }) } public addUnit (unit : ContentUnit) { this.units.push(unit); return this; } public reset () { this.matching = this.units.slice(); } public isMatching () { return this.matching.length; } public setMatching (matching : Array) { this.matching = matching; } public isMatch (cg : ContentGroup) : ContentGroupMatchResult { let unmatched = cg.matching.slice(); let matching = this.units.slice(); for (let i = matching.length - 1; i >= 0; i--) { for (let k = unmatched.length - 1; k >= 0; k--) { if (matching[i].isMatch(unmatched[k])) { unmatched.splice(k, 1); matching.splice(i, 1); break; } } } return { type : matching.length > 0 ? ContentGroupMatch.NO_MATCH : unmatched.length == 0 ? ContentGroupMatch.PERFECT_MATCH : ContentGroupMatch.PARTIAL_MATCH, unmatched : unmatched } } public getScore () { let score = 0; this.units.forEach(unit => { score += unit.getScore(); }); return Math.floor((score / this.units.length) * 10) + this.units.length; // If we keep precision too high, random won't work. } public matchAgainst (a : Array) : Array { let matches = []; this.reset(); for (let i = 0; i < a.length; i++) { let match = a[i].isMatch(this); if (match.type != ContentGroupMatch.NO_MATCH) { matches.push(i); this.setMatching(match.unmatched); } if (!this.isMatching()) { return matches; } } return undefined; } }