fix(core): Update build scripts, refine testing assertions, and enhance documentation
This commit is contained in:
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartfuzzy',
|
||||
version: '1.1.8',
|
||||
version: '1.1.9',
|
||||
description: 'A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.'
|
||||
}
|
||||
|
@ -1,37 +1,177 @@
|
||||
import * as plugins from './smartfuzzy.plugins.js';
|
||||
|
||||
/**
|
||||
* an article search that searches articles in a weighted manner
|
||||
* Type for the search result returned by ArticleSearch
|
||||
*/
|
||||
export type IArticleSearchResult = {
|
||||
/** The matched article */
|
||||
item: plugins.tsclass.content.IArticle;
|
||||
|
||||
/** The index of the article in the original array */
|
||||
refIndex: number;
|
||||
|
||||
/** The match score (lower is better) */
|
||||
score?: number;
|
||||
|
||||
/** Information about where matches were found in the article */
|
||||
matches?: ReadonlyArray<{
|
||||
indices: ReadonlyArray<readonly [number, number]>;
|
||||
key?: string;
|
||||
value?: string;
|
||||
refIndex?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialized search engine for articles with weighted field searching
|
||||
*
|
||||
* This class provides fuzzy searching against article content, with different weights
|
||||
* assigned to different parts of the article (title, tags, content) to provide
|
||||
* more relevant results.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const articles = [
|
||||
* {
|
||||
* title: 'Getting Started with TypeScript',
|
||||
* content: 'TypeScript is a superset of JavaScript that adds static typing...',
|
||||
* tags: ['typescript', 'javascript', 'programming'],
|
||||
* author: 'John Doe',
|
||||
* timestamp: Date.now(),
|
||||
* featuredImageUrl: null,
|
||||
* url: 'https://example.com/typescript-intro'
|
||||
* }
|
||||
* ];
|
||||
*
|
||||
* const articleSearch = new ArticleSearch(articles);
|
||||
* const results = await articleSearch.search('typescript');
|
||||
* ```
|
||||
*/
|
||||
export class ArticleSearch {
|
||||
/**
|
||||
* Collection of articles to search through
|
||||
*/
|
||||
public articles: plugins.tsclass.content.IArticle[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating whether the search index needs to be updated
|
||||
*/
|
||||
public needsUpdate: boolean = false;
|
||||
|
||||
/**
|
||||
* Promise manager for async operations
|
||||
*/
|
||||
private readyDeferred = plugins.smartpromise.defer();
|
||||
|
||||
/**
|
||||
* Fuse.js instance for searching
|
||||
*/
|
||||
private fuse: plugins.fuseJs<plugins.tsclass.content.IArticle>;
|
||||
|
||||
/**
|
||||
* Creates a new ArticleSearch instance
|
||||
*
|
||||
* @param articleArrayArg - Optional array of articles to initialize with
|
||||
*/
|
||||
constructor(articleArrayArg?: plugins.tsclass.content.IArticle[]) {
|
||||
// Validate input if provided
|
||||
if (articleArrayArg !== undefined && !Array.isArray(articleArrayArg)) {
|
||||
throw new Error('Article array must be an array');
|
||||
}
|
||||
|
||||
this.fuse = new plugins.fuseJs(this.articles);
|
||||
this.readyDeferred.resolve();
|
||||
|
||||
if (articleArrayArg) {
|
||||
for (const article of articleArrayArg) {
|
||||
// Validate each article has required fields
|
||||
if (!article || typeof article !== 'object') {
|
||||
throw new Error('Each article must be a valid object');
|
||||
}
|
||||
|
||||
// Require at least title field
|
||||
if (!article.title || typeof article.title !== 'string') {
|
||||
throw new Error('Each article must have a title string');
|
||||
}
|
||||
|
||||
this.addArticle(article);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allows adding an article
|
||||
* Adds an article to the collection and marks the index for updating
|
||||
*
|
||||
* @param articleArg - The article to add to the search collection
|
||||
* @returns void
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* articleSearch.addArticle({
|
||||
* title: 'Advanced TypeScript Features',
|
||||
* content: 'This article covers advanced TypeScript concepts...',
|
||||
* tags: ['typescript', 'advanced'],
|
||||
* author: 'Jane Smith',
|
||||
* timestamp: Date.now(),
|
||||
* featuredImageUrl: null,
|
||||
* url: 'https://example.com/advanced-typescript'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
addArticle(articleArg: plugins.tsclass.content.IArticle) {
|
||||
public addArticle(articleArg: plugins.tsclass.content.IArticle): void {
|
||||
if (!articleArg || typeof articleArg !== 'object') {
|
||||
throw new Error('Article must be a valid object');
|
||||
}
|
||||
|
||||
// Require at least title field
|
||||
if (!articleArg.title || typeof articleArg.title !== 'string') {
|
||||
throw new Error('Article must have a title string');
|
||||
}
|
||||
|
||||
// Validate tags if present
|
||||
if (articleArg.tags !== undefined && !Array.isArray(articleArg.tags)) {
|
||||
throw new Error('Article tags must be an array of strings');
|
||||
}
|
||||
|
||||
this.articles.push(articleArg);
|
||||
this.needsUpdate = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* allows searching an article
|
||||
* Performs a weighted fuzzy search across all articles
|
||||
*
|
||||
* The search uses the following weighting:
|
||||
* - Title: 3x importance
|
||||
* - Tags: 2x importance
|
||||
* - Content: 1x importance
|
||||
*
|
||||
* @param searchStringArg - The search query string
|
||||
* @returns Array of articles matched with their relevance score and match details
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Search for articles about TypeScript
|
||||
* const results = await articleSearch.search('typescript');
|
||||
*
|
||||
* // Access the first (most relevant) result
|
||||
* if (results.length > 0) {
|
||||
* console.log(results[0].item.title);
|
||||
*
|
||||
* // See where the match was found
|
||||
* console.log(results[0].matches);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public async search(searchStringArg: string) {
|
||||
public async search(searchStringArg: string): Promise<IArticleSearchResult[]> {
|
||||
if (typeof searchStringArg !== 'string') {
|
||||
throw new Error('Search string must be a string');
|
||||
}
|
||||
|
||||
// Empty article collection should return empty results
|
||||
if (this.articles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.needsUpdate) {
|
||||
const oldDeferred = this.readyDeferred;
|
||||
this.readyDeferred = plugins.smartpromise.defer();
|
||||
|
@ -1,18 +1,107 @@
|
||||
import * as plugins from './smartfuzzy.plugins.js';
|
||||
|
||||
/**
|
||||
* Result of a fuzzy search on objects
|
||||
*
|
||||
* @typeParam T - The type of object being searched
|
||||
*/
|
||||
export interface IFuzzySearchResult<T> {
|
||||
/** The matched object */
|
||||
item: T;
|
||||
|
||||
/** The index of the object in the original array */
|
||||
refIndex: number;
|
||||
|
||||
/** The match score (lower is better) */
|
||||
score?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fuzzy searching and sorting of objects based on their properties
|
||||
*
|
||||
* @typeParam T - The type of objects to search through
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* interface User {
|
||||
* name: string;
|
||||
* email: string;
|
||||
* }
|
||||
*
|
||||
* const users = [
|
||||
* { name: 'John Smith', email: 'john@example.com' },
|
||||
* { name: 'Jane Doe', email: 'jane@example.com' }
|
||||
* ];
|
||||
*
|
||||
* const sorter = new ObjectSorter<User>(users);
|
||||
* const results = sorter.sort('john', ['name', 'email']);
|
||||
* ```
|
||||
*/
|
||||
export class ObjectSorter<T> {
|
||||
/**
|
||||
* The collection of objects to search through
|
||||
*/
|
||||
public objectDictionary: T[];
|
||||
|
||||
/**
|
||||
* Creates a new ObjectSorter instance
|
||||
*
|
||||
* @param objectDictionaryArg - Array of objects to search through
|
||||
*/
|
||||
constructor(objectDictionaryArg: T[] = []) {
|
||||
if (objectDictionaryArg !== undefined && !Array.isArray(objectDictionaryArg)) {
|
||||
throw new Error('Object dictionary must be an array');
|
||||
}
|
||||
this.objectDictionary = objectDictionaryArg;
|
||||
}
|
||||
|
||||
sort(stringArg: string, objectKeysArg: string[]): Array<{ item: T; refIndex: number; score?: number }> {
|
||||
/**
|
||||
* Searches and sorts objects based on how well they match the search string
|
||||
* in the specified object properties
|
||||
*
|
||||
* @param stringArg - The search query string
|
||||
* @param objectKeysArg - Array of object property names to search within
|
||||
* @returns Array of results sorted by relevance (best matches first)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Search for 'john' in both name and email fields
|
||||
* const results = sorter.sort('john', ['name', 'email']);
|
||||
*
|
||||
* // First result is the best match
|
||||
* console.log(results[0].item.name); // 'John Smith'
|
||||
* ```
|
||||
*/
|
||||
public sort(stringArg: string, objectKeysArg: string[]): Array<IFuzzySearchResult<T>> {
|
||||
if (typeof stringArg !== 'string') {
|
||||
throw new Error('Search string must be a string');
|
||||
}
|
||||
|
||||
if (!Array.isArray(objectKeysArg)) {
|
||||
throw new Error('Object keys must be an array');
|
||||
}
|
||||
|
||||
if (objectKeysArg.length === 0) {
|
||||
throw new Error('At least one object key must be provided for searching');
|
||||
}
|
||||
|
||||
// Verify all keys are strings
|
||||
for (const key of objectKeysArg) {
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error('All object keys must be strings');
|
||||
}
|
||||
}
|
||||
|
||||
// Empty dictionary should return empty results instead of error
|
||||
if (this.objectDictionary.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fuseOptions = {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
threshold: 0.6, // Lower values = more strict matching
|
||||
location: 0, // Where to start searching in the string
|
||||
distance: 100, // How far to search in the string
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: objectKeysArg,
|
||||
|
@ -2,31 +2,93 @@ import * as plugins from './smartfuzzy.plugins.js';
|
||||
|
||||
export let standardExport = 'Hi there! :) This is an exported string';
|
||||
|
||||
/**
|
||||
* Type representing a dictionary of words mapped to their scores
|
||||
* Lower scores typically indicate better matches
|
||||
*/
|
||||
export type TDictionaryMap = { [key: string]: number };
|
||||
|
||||
/**
|
||||
* Main class for fuzzy string matching against a dictionary
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fuzzy = new Smartfuzzy(['apple', 'banana', 'orange']);
|
||||
* const result = fuzzy.findClosestMatch('aple'); // Returns 'apple'
|
||||
* ```
|
||||
*/
|
||||
export class Smartfuzzy {
|
||||
dictionary: string[];
|
||||
/**
|
||||
* Array of words used for fuzzy matching
|
||||
*/
|
||||
public dictionary: string[];
|
||||
|
||||
/**
|
||||
* Creates a new Smartfuzzy instance
|
||||
*
|
||||
* @param dictionary - Initial array of words to use for matching
|
||||
*/
|
||||
constructor(dictionary: string[]) {
|
||||
if (!Array.isArray(dictionary)) {
|
||||
throw new Error('Dictionary must be an array of strings');
|
||||
}
|
||||
this.dictionary = dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds words to the dictionary
|
||||
* @param payloadArg
|
||||
* Adds one or more words to the dictionary
|
||||
*
|
||||
* @param payloadArg - A single word or an array of words to add to the dictionary
|
||||
* @returns void
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* fuzzy.addToDictionary('pear');
|
||||
* fuzzy.addToDictionary(['pear', 'grape', 'kiwi']);
|
||||
* ```
|
||||
*/
|
||||
addToDictionary(payloadArg: string | string[]) {
|
||||
public addToDictionary(payloadArg: string | string[]): void {
|
||||
if (payloadArg === undefined || payloadArg === null) {
|
||||
throw new Error('Input cannot be null or undefined');
|
||||
}
|
||||
|
||||
if (Array.isArray(payloadArg)) {
|
||||
// Validate all items in array are strings
|
||||
for (const item of payloadArg) {
|
||||
if (typeof item !== 'string') {
|
||||
throw new Error('All items in array must be strings');
|
||||
}
|
||||
}
|
||||
this.dictionary = this.dictionary.concat(payloadArg);
|
||||
} else {
|
||||
} else if (typeof payloadArg === 'string') {
|
||||
this.dictionary.push(payloadArg);
|
||||
} else {
|
||||
throw new Error('Input must be a string or an array of strings');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the closest match for a given string
|
||||
* @param stringArg
|
||||
* Calculates the Levenshtein distance (edit distance) between the input string
|
||||
* and each word in the dictionary
|
||||
*
|
||||
* @param stringArg - The string to compare against the dictionary
|
||||
* @returns A dictionary map where keys are words and values are edit distances
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const scores = fuzzy.calculateScores('aple');
|
||||
* // Returns: { 'apple': 1, 'banana': 5, 'orange': 5 }
|
||||
* ```
|
||||
*/
|
||||
getChangeScoreForString(stringArg: string): TDictionaryMap {
|
||||
public calculateScores(stringArg: string): TDictionaryMap {
|
||||
if (typeof stringArg !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
|
||||
if (this.dictionary.length === 0) {
|
||||
throw new Error('Dictionary is empty');
|
||||
}
|
||||
|
||||
const dictionaryMap: TDictionaryMap = {};
|
||||
for (const wordArg of this.dictionary) {
|
||||
dictionaryMap[wordArg] = plugins.leven(stringArg, wordArg);
|
||||
@ -34,7 +96,34 @@ export class Smartfuzzy {
|
||||
return dictionaryMap;
|
||||
}
|
||||
|
||||
getClosestMatchForString(stringArg: string): string {
|
||||
/**
|
||||
* @deprecated Use calculateScores instead
|
||||
*/
|
||||
public getChangeScoreForString(stringArg: string): TDictionaryMap {
|
||||
return this.calculateScores(stringArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest matching word in the dictionary using fuzzy search
|
||||
*
|
||||
* @param stringArg - The string to find a match for
|
||||
* @returns The closest matching word, or null if no match is found or dictionary is empty
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const match = fuzzy.findClosestMatch('oragne');
|
||||
* // Returns: 'orange'
|
||||
* ```
|
||||
*/
|
||||
public findClosestMatch(stringArg: string): string {
|
||||
if (typeof stringArg !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
|
||||
if (this.dictionary.length === 0) {
|
||||
return null; // Return null for empty dictionary instead of throwing error
|
||||
}
|
||||
|
||||
const fuseDictionary: { name: string }[] = [];
|
||||
for (const wordArg of this.dictionary) {
|
||||
fuseDictionary.push({
|
||||
@ -58,4 +147,11 @@ export class Smartfuzzy {
|
||||
}
|
||||
return closestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use findClosestMatch instead
|
||||
*/
|
||||
public getClosestMatchForString(stringArg: string): string {
|
||||
return this.findClosestMatch(stringArg);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user