Skip to content

Commit

Permalink
Cleanup search and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DTCurrie committed Aug 25, 2023
1 parent 18600a6 commit 2a8798b
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 22 deletions.
139 changes: 139 additions & 0 deletions packages/core/src/lib/select/__tests__/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, expect, it } from 'vitest';
import {
getSearchHighlight,
getSearchMatch,
getSearchMatches,
getSearchResults,
getWordIndex,
prioritizeMatches,
} from '../search';
import { escapeRegExp } from 'lodash';

describe('search', () => {
const options = [
'First Option',
'Option 2',
'C.) Option',
'Something Else',
'With A Whole Lot Of Parts',
];

describe('getWordIndex', () => {
it('should return the index of the word containing the character at the passed index', () => {
const option = 'With A Whole Lot Of Parts';
const index = option.indexOf('c');
expect(getWordIndex(option, index)).toBe(5);
});
});

describe('getSearchHighlight', () => {
const option = 'Test Option';

it('should return a highlighting breakdown of the passed option with a term matching the start of a word', () => {
const search = 'Opt';
const match = new RegExp(escapeRegExp(search), 'giu').exec(option);
const highlight = getSearchHighlight(option, search, match!);

expect(highlight?.[0]).toBe('Test ');
expect(highlight?.[1]).toBe(search);
expect(highlight?.[2]).toBe('ion');
});

it('should return a highlighting breakdown of the passed option with a term matching anywhere', () => {
const search = 'pt';
const match = new RegExp(escapeRegExp(search), 'giu').exec(option);
const highlight = getSearchHighlight(option, search, match!);

expect(highlight?.[0]).toBe('Test O');
expect(highlight?.[1]).toBe(search);
expect(highlight?.[2]).toBe('ion');
});
});

describe('getSearchMatch', () => {
const option = 'Test Option';

it('should return a priority 0 match (start of word)', () => {
const search = 'Opt';
const match = getSearchMatch(option, search);

expect(match.priority).toBe(0);
});

it('should return a priority 1 match (in first word)', () => {
const search = 'est';
const match = getSearchMatch(option, search);

expect(match.priority).toBe(1);
});

it('should return a priority 2 match (in second word)', () => {
const search = 'pt';
const match = getSearchMatch(option, search);

expect(match.priority).toBe(2);
});

it('should return a priority -1 match (no match)', () => {
const search = 'rf';
const match = getSearchMatch(option, search);

expect(match.priority).toBe(-1);
});
});

describe('prioritizeMatches', () => {
it('should return a prioritized map of matches', () => {
const matches = prioritizeMatches(options, 's');

expect(matches['-1'].length).toBe(2);
expect(matches['0'].length).toBe(1);
expect(matches['1']?.length).toBe(1);
expect(matches['6']?.length).toBe(1);
});
});

describe('getSearchMatches', () => {
it('should return an array of search matches sorted by their priority', () => {
const matches = getSearchMatches(options, 's', false);

expect(matches[0]?.priority).toBe(0);
expect(matches[1]?.priority).toBe(1);
expect(matches[2]?.priority).toBe(6);
expect(matches[3]?.priority).toBe(-1);
expect(matches[4]?.priority).toBe(-1);
});

it('should return an array of search matches sorted by their priority without any non-matches', () => {
const matches = getSearchMatches(options, 's', true);

expect(matches[0]?.priority).toBe(0);
expect(matches[1]?.priority).toBe(1);
expect(matches[2]?.priority).toBe(6);
expect(matches[3]).toBe(undefined);
expect(matches[4]).toBe(undefined);
});
});

describe('getSearchResults', () => {
it('should return an array of search results sorted by priority', () => {
const matches = getSearchResults(options, 's', false);

expect(matches[0]?.option).toBe('Something Else');
expect(matches[1]?.option).toBe('First Option');
expect(matches[2]?.option).toBe('With A Whole Lot Of Parts');
expect(matches[3]?.option).toBe('Option 2');
expect(matches[4]?.option).toBe('C.) Option');
});

it('should return an array of search results sorted by priority without any non-matches', () => {
const matches = getSearchResults(options, 's', true);

expect(matches[0]?.option).toBe('Something Else');
expect(matches[1]?.option).toBe('First Option');
expect(matches[2]?.option).toBe('With A Whole Lot Of Parts');
expect(matches[3]).toBe(undefined);
expect(matches[4]).toBe(undefined);
});
});
});
39 changes: 17 additions & 22 deletions packages/core/src/lib/select/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type SearchMatches = {
* Returns the index of the word where the character with the passed index
* occurs.
*/
const getWordIndex = (option: string, index: number) => {
export const getWordIndex = (option: string, index: number) => {
const preceding = option.slice(0, index);
return (preceding.match(/\s/gu) || []).length;
};
Expand All @@ -74,16 +74,15 @@ const getWordIndex = (option: string, index: number) => {
* Returns the passed option broken down into segments to apply highlighting
* to the portion of the option that matches the passed search term.
*/
const getSearchHighlight = (
export const getSearchHighlight = (
option: string,
searchTerm: string,
{ index }: RegExpExecArray
): SearchHighlight | undefined => {
const matchIndex = index === 0 ? 0 : index + 1;
return [
option.slice(0, matchIndex),
option.slice(matchIndex, matchIndex + searchTerm.length),
option.slice(matchIndex + searchTerm.length),
option.slice(0, index),
option.slice(index, index + searchTerm.length),
option.slice(index + searchTerm.length),
];
};

Expand All @@ -94,7 +93,10 @@ const getSearchHighlight = (
* - The index of the word in the option that matched
* - `-1` for non-matches
*/
const getSearchMatch = (option: string, searchTerm: string): SearchMatch => {
export const getSearchMatch = (
option: string,
searchTerm: string
): SearchMatch => {
// Match on the initial character of any word in the option
const initialCharacterMatch = new RegExp(
`(^${searchTerm}|\\s${searchTerm})`,
Expand Down Expand Up @@ -136,7 +138,7 @@ const getSearchMatch = (option: string, searchTerm: string): SearchMatch => {
* Checks each passed option if it matches with the passed search term, and
* returns a prioritized map of the results.
*/
const prioritizeMatches = (options: string[], searchTerm: string) => {
export const prioritizeMatches = (options: string[], searchTerm: string) => {
const results: SearchMatches = {
'0': [],
'-1': [],
Expand All @@ -157,7 +159,11 @@ const prioritizeMatches = (options: string[], searchTerm: string) => {
* highlighting breakdown for the match. If reduce is true, options
* with no match (-1 priority) will not be included in the results.
*/
const getMatches = (options: string[], searchTerm: string, reduce: boolean) => {
export const getSearchMatches = (
options: string[],
searchTerm: string,
reduce: boolean
) => {
const prioritized = prioritizeMatches(options, searchTerm);
const results: SearchMatch[] = [];

Expand All @@ -174,16 +180,6 @@ const getMatches = (options: string[], searchTerm: string, reduce: boolean) => {
return results;
};

/**
* Sorts the passed results by the `highlight` segment of the result.
*/
const sortByHighlight = (results: SearchMatch[]) =>
results.sort((a, b) => {
const aIndex = a.highlight?.[1] ?? -1;
const bIndex = b.highlight?.[1] ?? -1;
return aIndex < bIndex ? -1 : 1;
});

/**
* Searches against the passed options with the passed search term before
* sorting them and breaking them down into segments to allow for highlighting.
Expand All @@ -209,7 +205,7 @@ export const getSearchResults = (
const matches: SearchMatch[] = [];
const noMatches = [];
const escaped = escapeRegExp(searchTerm);
const results = getMatches(options, escaped, reduce);
const results = getSearchMatches(options, escaped, reduce);

for (const match of results) {
if (match.highlight === undefined) {
Expand All @@ -220,6 +216,5 @@ export const getSearchResults = (
matches.push(match);
}

const sorted = sortByHighlight(matches);
return [...sorted, ...noMatches];
return [...matches, ...noMatches];
};

0 comments on commit 2a8798b

Please sign in to comment.