Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle nested arrays with wildcard keys #120

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,25 @@ const nestedObjList = [
]
matchSorter(nestedObjList, 'j', {keys: ['name.0.first']})
// [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]

// matchSorter(nestedObjList, 'j', {keys: ['name[0].first']}) does not work
```

This even works with arrays of multiple nested objects: just specify the key
using dot-notation with the `*` wildcard instead of a numeric index.

```javascript
const nestedObjList = [
{aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]},
{aliases: [{name: {first: 'Fred'}},{name: {first: 'Frederic'}}]},
{aliases: [{name: {first: 'George'}},{name: {first: 'Georgie'}}]},
]
matchSorter(nestedObjList, 'jen', {keys: ['aliases.*.name.first']})
// [{aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]}]
matchSorter(nestedObjList, 'jen', {keys: ['aliases.0.name.first']})
// []
```

**Property Callbacks**: Alternatively, you may also pass in a callback function
that resolves the value of the key(s) you wish to match on. This is especially
useful when interfacing with libraries such as Immutable.js
Expand Down
62 changes: 61 additions & 1 deletion src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const tests: Record<string, TestCase> = {
],
output: [{name: 'A', age: 0}],
},
'can handle objected with nested keys': {
'can handle object with nested keys': {
input: [
[
{name: {first: 'baz'}},
Expand All @@ -129,6 +129,36 @@ const tests: Record<string, TestCase> = {
],
output: [{name: {first: 'bat'}}, {name: {first: 'baz'}}],
},
'can handle object with an array of values with nested keys with a specific index': {
input: [
[
{aliases: [{name: {first: 'baz'}},{name: {first: 'foo'}},{name: null}]},
{aliases: [{name: {first: 'foo'}},{name: {first: 'bat'}},null]},
{aliases: [{name: {first: 'foo'}},{name: {first: 'foo'}}]},
{aliases: null},
{},
null,
],
'ba',
{keys: ['aliases.0.name.first']},
],
output: [{aliases: [{name: {first: 'baz'}},{name: {first: 'foo'}},{name: null}]}],
},
'can handle object with an array of values with nested keys with a wildcard': {
input: [
[
{aliases: [{name: {first: 'baz'}},{name: {first: 'foo'}},{name: null}]},
{aliases: [{name: {first: 'foo'}},{name: {first: 'bat'}},null]},
{aliases: [{name: {first: 'foo'}},{name: {first: 'foo'}}]},
{aliases: null},
{},
null,
],
'ba',
{keys: ['aliases.*.name.first']},
],
output: [{aliases: [{name: {first: 'baz'}},{name: {first: 'foo'}},{name: null}]}, {aliases: [{name: {first: 'foo'}},{name: {first: 'bat'}},null]}],
},
'can handle property callback': {
input: [
[{name: {first: 'baz'}}, {name: {first: 'bat'}}, {name: {first: 'foo'}}],
Expand All @@ -153,6 +183,36 @@ const tests: Record<string, TestCase> = {
{favoriteIceCream: ['mint', 'chocolate']},
],
},
'can handle keys that are an array of values with a wildcard': {
input: [
[
{favoriteIceCream: ['mint', 'chocolate']},
{favoriteIceCream: ['candy cane', 'brownie']},
{favoriteIceCream: ['birthday cake', 'rocky road', 'strawberry']},
],
'cc',
{keys: ['favoriteIceCream.*']},
],
output: [
{favoriteIceCream: ['candy cane', 'brownie']},
{favoriteIceCream: ['mint', 'chocolate']},
],
},
'can handle nested keys that are an array of values with a wildcard': {
input: [
[
{favorite: {iceCream: ['mint', 'chocolate']}},
{favorite: {iceCream: ['candy cane', 'brownie']}},
{favorite: {iceCream: ['birthday cake', 'rocky road', 'strawberry']}},
],
'cc',
{keys: ['favorite.iceCream.*']},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this working, but I'd also like to support favorite.iceCream here as well. We should be able to process an item of String | Array<String> and favorite.icCream is Array<String>.

],
output: [
{favorite: {iceCream: ['candy cane', 'brownie']}},
{favorite: {iceCream: ['mint', 'chocolate']}},
],
},
'can handle keys with a maxRanking': {
input: [
[
Expand Down
79 changes: 51 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ function getClosenessRanking(testString: string, stringToRank: string): number {
string: string,
index: number,
) {
for (let j = index; j < string.length; j++) {
for (let j = index, J = string.length; j < J; j++) {
const stringChar = string[j]
if (stringChar === matchChar) {
matchingInOrderCharCount += 1
Expand All @@ -280,7 +280,7 @@ function getClosenessRanking(testString: string, stringToRank: string): number {
return rankings.NO_MATCH
}
charNumber = firstIndex
for (let i = 1; i < stringToRank.length; i++) {
for (let i = 1, I = stringToRank.length; i < I; i++) {
const matchChar = stringToRank[i]
charNumber = findMatchingCharacter(matchChar, testString, charNumber)
const found = charNumber > -1
Expand Down Expand Up @@ -349,42 +349,67 @@ function prepareValueForComparison<ItemType>(
function getItemValues<ItemType>(
item: ItemType,
key: KeyOption<ItemType>,
): Array<string> | null {
): Array<string> {
if (typeof key === 'object') {
key = key.key as string
}
let value: string | Array<string> | null
if (typeof key === 'function') {
value = key(item)
// eslint-disable-next-line no-negated-condition
} else if (item == null) {
value = null
} else if (Object.hasOwnProperty.call(item, key)) {
// @ts-expect-error just like below...
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value = item[key];
} else {
value = getNestedValue<ItemType>(key, item)
}
const values: Array<string> = []
// concat because `value` can be a string or an array
// eslint-disable-next-line
return value != null ? values.concat(value) : null

// because `value` can also be undefined
if (value == null) {
return []
}
if (Array.isArray(value)) {
return value
}
return [value]
}

/**
* Given key: "foo.bar.baz"
* And obj: {foo: {bar: {baz: 'buzz'}}}
* And item: {foo: {bar: {baz: 'buzz'}}}
* -> 'buzz'
* @param key a dot-separated set of keys
* @param obj the object to get the value from
* @param item the item to get the value from
*/
function getNestedValue<ItemType>(
key: string,
obj: ItemType,
item: ItemType,
): string | Array<string> | null {
// @ts-expect-error really have no idea how to type this properly...
return key.split('.').reduce((itemObj: object | null, nestedKey: string):
| object
| string
| null => {
// @ts-expect-error lost on this one as well...
return itemObj ? itemObj[nestedKey] : null
}, obj)
return key.split('.').reduce((nestedItems: Array<object | string | null>, nestedKey: string): Array<object | string> => {
return nestedItems.reduce((values: Array<object | string>, nestedItem: Array<object | string> | object | string | null): Array<object | string> => {
if (nestedItem == null) {
return values
}

if (nestedKey === "*") {
return values.concat(nestedItem)
}

if (Object.hasOwnProperty.call(nestedItem,nestedKey)) {
// @ts-expect-error and here again...
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const nestedValue = nestedItem[nestedKey]
if (nestedValue != null) {
values.push(nestedValue)
}
}

return values
}, [])
}, [item])
}

/**
Expand All @@ -398,17 +423,15 @@ function getAllValuesToRank<ItemType>(
keys: Array<KeyOption<ItemType>>,
) {
return keys.reduce<Array<{itemValue: string; attributes: KeyAttributes}>>(
(allVals, key) => {
const values = getItemValues(item, key)
if (values) {
values.forEach(itemValue => {
allVals.push({
itemValue,
attributes: getKeyAttributes(key),
})
(allValues, key) => {
const attributes = getKeyAttributes(key)
getItemValues(item, key).forEach(itemValue => {
allValues.push({
itemValue,
attributes,
})
}
return allVals
})
return allValues
},
[],
)
Expand Down