Skip to content

Commit

Permalink
Merge pull request #75 from mlbonniec/72-use-generic-type
Browse files Browse the repository at this point in the history
[feature] add support for generic type
  • Loading branch information
SMAKSS authored Apr 9, 2024
2 parents f2161cd + c40396e commit 08bfb76
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 79 deletions.
41 changes: 26 additions & 15 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,48 +42,59 @@ The `Search` function accepts an options object with the following properties:
- `include` (`Boolean`): Determines whether to include (`true`) or exclude (`false`) the keys specified. Defaults to `true`.
- `exact` (`Boolean`): Determines whether to perform an exact match search. Defaults to `false`.

The search function can take a generic type to specify the type of the search items.

## Examples of Usage

Lets suppose we have the following type:

```ts
type Person = { name: string; lastName: string };
```

### Searching Within an Object

When the match is found in an object, the entire object is returned:

```js
const obj = { name: 'John', lastName: 'Doe' };
```ts
const obj: Person = { name: 'John', lastName: 'Doe' };

const results = Search({ searchText: 'john', searchItems: [obj] });
const results = Search<Person>({ searchText: 'john', searchItems: [obj] });
// Results: [{ name: 'John', lastName: 'Doe' }]
```

### Searching Within an Array

```js
const arr = [
```ts
const arr: Person[] = [
{ name: 'John', lastName: 'Doe' },
{ name: 'Joe', lastName: 'Doe' }
];

const results = Search({ searchText: 'john', searchItems: arr });
const results = Search<Person>({ searchText: 'john', searchItems: arr });
// Results: [{ name: 'John', lastName: 'Doe' }]
```

### Searching Within a Nested Array

```js
const nestedArr = [
```ts
const nestedArr: (Person | Person[])[] = [
{ name: 'John', lastName: 'Doe' },
{ name: 'Joe', lastName: 'Doe' },
[{ name: 'Jane', lastName: 'Doe' }]
];

const results = Search({ searchText: 'jane', searchItems: nestedArr });
const results = Search<Person | Person[]>({
searchText: 'jane',
searchItems: nestedArr
});
// Results: [{ name: 'Jane', lastName: 'Doe' }]
```

### Including Specific Keys

```js
const results = Search({
```ts
const results = Search<Person>({
searchText: 'jane',
searchItems: nestedArr,
keys: ['name']
Expand All @@ -93,8 +104,8 @@ const results = Search({

### Excluding Specific Keys

```js
const results = Search({
```ts
const results = Search<Person>({
searchText: 'jane',
searchItems: nestedArr,
keys: ['lastName'],
Expand All @@ -107,8 +118,8 @@ _Note: The result is an empty array because 'lastName' is excluded from the sear

### Performing an Exact Match Search

```js
const results = Search({
```ts
const results = Search<Person>({
searchText: 'jane',
searchItems: nestedArr,
exact: true
Expand Down
22 changes: 10 additions & 12 deletions src/search-functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,44 @@
import { searchWithinObject, recursiveSearch } from './search-functions'; // Adjust import path
import { SearchItem } from './types';

type Person = { name: string; lastName: string };

describe('searchWithinObject', () => {
it('should add object to results if a match is found', () => {
const person: SearchItem = { name: 'John', lastName: 'Doe' };
const results: SearchItem[] = [];
const person: Person = { name: 'John', lastName: 'Doe' };
const results: Person[] = [];
searchWithinObject(person, ['name'], true, /John/i, results);
expect(results).toContain(person);
});

it('should not add object to results if no match is found', () => {
const person: SearchItem = { name: 'John', lastName: 'Doe' };
const results: SearchItem[] = [];
const person: Person = { name: 'John', lastName: 'Doe' };
const results: Person[] = [];
searchWithinObject(person, ['name'], true, /Jane/i, results);
expect(results).toEqual([]);
});
});

describe('recursiveSearch', () => {
it('should add all matching items from an array', () => {
const people: SearchItem[] = [
const people: Person[] = [
{ name: 'John', lastName: 'Doe' },
{ name: 'Jane', lastName: 'Doe' }
];
const results: SearchItem[] = [];
const results: Person[] = [];
recursiveSearch(people, ['lastName'], true, /Doe/i, results);
expect(results).toEqual(expect.arrayContaining(people));
});

it('should work recursively through nested arrays/objects', () => {
const nestedPeople: (
| SearchItem
| (SearchItem & { contacts: SearchItem[] })
)[] = [
const nestedPeople: (Person | (Person & { contacts: Person[] }))[] = [
{
name: 'John',
lastName: 'Doe',
contacts: [{ name: 'Jake', lastName: 'Doe' }]
},
{ name: 'Jane', lastName: 'Doe' }
];
const results: SearchItem[] = [];
const results: Person[] = [];
recursiveSearch(nestedPeople, ['lastName'], true, /Doe/i, results);
expect(results.length).toBe(2);
});
Expand Down
28 changes: 15 additions & 13 deletions src/search-functions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { SearchItem } from './types';
import type { SearchItem, KeyOf } from './types';
import { addUniqueMatch, isKeyIncluded } from './utils';

/**
* Searches for matches within an object based on a regex pattern.
* If a match is found within the specified keys, it adds the object to the results.
*
* @param {SearchItem} object - The object to search within.
* @template T - The type of the object to search within, extending SearchItem.
* @param {T} object - The object to search within.
* @param {string[]} keys - The keys to include or exclude in the search.
* @param {boolean} include - Whether to include or exclude the specified keys in the search.
* @param {RegExp} regex - The regex pattern to match against the object values.
* @param {SearchItem[]} results - The array to store matching objects.
* @param {T[]} results - The array to store matching objects.
*
* @example
* // Define an object to search
Expand All @@ -22,12 +23,12 @@ import { addUniqueMatch, isKeyIncluded } from './utils';
* // results will contain the person object
* console.log(results); // [{ name: "John", lastName: "Doe" }]
*/
export function searchWithinObject(
object: SearchItem,
keys: string[],
export function searchWithinObject<T extends SearchItem>(
object: T,
keys: KeyOf<T>[],
include: boolean,
regex: RegExp,
results: SearchItem[]
results: T[]
): void {
for (const key of Object.keys(object)) {
if (isKeyIncluded(key, keys, include) && regex.test(String(object[key]))) {
Expand All @@ -41,7 +42,8 @@ export function searchWithinObject(
* Recursively searches through items for matches based on a regex pattern.
* It handles both arrays and individual objects.
*
* @param {SearchItem | SearchItem[]} items - The items to search through. Can be a single item or an array of items.
* @template T - The type of the items to search through, extending SearchItem.
* @param {T | T[]} items - The items to search through. Can be a single item or an array of items.
* @param {string[]} keys - The keys to include or exclude in the search.
* @param {boolean} include - Whether to include or exclude the specified keys in the search.
* @param {RegExp} regex - The regex pattern to match against item values.
Expand All @@ -61,12 +63,12 @@ export function searchWithinObject(
* // searchResults will contain both person objects
* console.log(searchResults); // [{ name: "John", lastName: "Doe" }, { name: "Jane", lastName: "Doe" }]
*/
export function recursiveSearch(
items: SearchItem | SearchItem[],
keys: string[],
export function recursiveSearch<T extends SearchItem>(
items: T | T[],
keys: KeyOf<T>[],
include: boolean,
regex: RegExp,
results: SearchItem[]
results: T[]
): void {
if (Array.isArray(items)) {
for (const item of items) {
Expand All @@ -75,6 +77,6 @@ export function recursiveSearch(
} else if (typeof items === 'object' && items !== null) {
searchWithinObject(items, keys, include, regex, results);
} else if (regex.test(String(items))) {
addUniqueMatch(results, [items]);
addUniqueMatch(results, items);
}
}
31 changes: 17 additions & 14 deletions src/search.test.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,63 @@
import { SearchOptions } from './types';
import search from './search';

type Person = { name: string; lastName: string; age: number };

describe('search', () => {
const people = [
const people: Person[] = [
{ name: 'John', lastName: 'Doe', age: 30 },
{ name: 'Jane', lastName: 'Smith', age: 25 },
{ name: 'Jake', lastName: 'Doe', age: 22 }
];

it('should find items by lastName', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'doe',
searchItems: people,
keys: ['lastName'],
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(2);
expect(results).toEqual(
expect.arrayContaining([expect.objectContaining({ lastName: 'Doe' })])
);
});

it('should handle exact matches', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'Doe',
searchItems: people,
keys: ['lastName'],
exact: true
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(2);
expect(results).toEqual(
expect.arrayContaining([expect.objectContaining({ lastName: 'Doe' })])
);
});

it('should be case insensitive', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'smith',
searchItems: people,
keys: ['lastName'],
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(1);
expect(results[0].lastName).toBe('Smith');
});

it('should return an empty array when no matches are found', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'nonexistent',
searchItems: people,
keys: ['lastName'],
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(0);
});

Expand All @@ -64,33 +67,33 @@ describe('search', () => {
searchItems: people,
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(1);
expect(results[0].age).toBe(25);
});

it('should include keys in the results if include is true', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'John',
searchItems: people,
keys: ['name'],
include: true,
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(1);
expect(results[0]).toHaveProperty('name', 'John');
});

it('should not include keys in the results if include is false', () => {
const options = {
const options: SearchOptions<Person> = {
searchText: 'Jane',
searchItems: people,
keys: ['name'],
include: false,
exact: false
};
const results = search(options);
const results: Person[] = search(options);
expect(results.length).toBe(0);
});
});
20 changes: 11 additions & 9 deletions src/search.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { SearchItem, SearchOptions } from './types';
import type { SearchItem, SearchOptions, KeyOf } from './types';
import { recursiveSearch } from './search-functions';

/**
* Searches for items within a collection that match the given search text.
*
* @param {SearchOptions} options - The search parameters including searchText, searchItems, keys to search in,
* whether to include keys and if the search is exact.
* @returns {SearchItem[]} The matched items as an array.
* @template T - The type of the items to search through, extending SearchItem.
* @returns {T[]} The matched items as an array.
*
* @example
* type Person = { name: string; lastName: string; }
* // Define a list of objects to search
* const people = [
* const people: Person[] = [
* { name: "John", lastName: "Doe" },
* { name: "Jane", lastName: "Smith" },
* ];
Expand All @@ -25,25 +27,25 @@ import { recursiveSearch } from './search-functions';
* };
*
* // Perform the search
* const found = search(options);
* const found = search<Person>(options);
*
* // found will contain the object with lastName 'Doe'
* console.log(found); // [{ name: "John", lastName: "Doe" }]
*/
function search({
function search<T extends SearchItem = SearchItem>({
searchText,
searchItems,
keys = [],
include = true,
exact = false
}: SearchOptions): SearchItem[] {
}: SearchOptions<T>): T[] {
const regex = new RegExp(exact ? `^${searchText}$` : searchText, 'i');
const results: SearchItem[] = [];
const results: T[] = [];

const preparedItems: SearchItem[] = Array.isArray(searchItems)
const preparedItems: T[] = Array.isArray(searchItems)
? searchItems
: [searchItems];
const preparedKeys: string[] =
const preparedKeys: KeyOf<T>[] =
keys.length > 0 ? keys : Object.keys(preparedItems[0] || {});

recursiveSearch(preparedItems, preparedKeys, include, regex, results);
Expand Down
Loading

0 comments on commit 08bfb76

Please sign in to comment.