Last active
April 19, 2024 20:58
-
-
Save nathanstilwell/3fc4b7a27441efb1725f937e633c9856 to your computer and use it in GitHub Desktop.
Something I was toying with that may not be a good idea.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
describe('groupByConfig', () => { | |
it('should group primitive values based on predicate', () => { | |
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; | |
const grouped = groupByConfig<number>( | |
{ predicate: (n) => n % 2 === 0, annotation: 'Even numbers' }, | |
{ predicate: (n) => n % 2 !== 0, annotation: 'Odd numbers' } | |
)(data); | |
expect(grouped).toEqual([ | |
{ list: [2, 4, 6, 8, 10], annotation: 'Even numbers' }, | |
{ list: [1, 3, 5, 7, 9], annotation: 'Odd numbers' }, | |
]); | |
}); | |
it('should group objects based on predicate', () => { | |
type Person = { name: string; age: number; gender: 'M' | 'F' | 'NB' }; | |
const data: Person[] = [ | |
{ name: 'Alice', age: 25, gender: 'F' }, | |
{ name: 'Mohammed', age: 30, gender: 'M' }, | |
{ name: 'Charlie', age: 35, gender: 'M' }, | |
{ name: 'Luther', age: 40, gender: 'M' }, | |
{ name: 'Eve', age: 45, gender: 'F' }, | |
{ name: 'Maria', age: 50, gender: 'F' }, | |
{ name: 'Valentina', age: 24, gender: 'NB' }, | |
]; | |
const grouped = groupByConfig<Person>( | |
{ predicate: (person) => person.age >= 40, annotation: 'Folks over 40' }, | |
{ | |
predicate: (person) => person.gender === 'F' || person.gender === 'M', | |
annotation: 'The Cis people under 40', | |
} | |
)(data); | |
const expected = [ | |
{ | |
list: [ | |
{ name: 'Luther', age: 40, gender: 'M' }, | |
{ name: 'Eve', age: 45, gender: 'F' }, | |
{ name: 'Maria', age: 50, gender: 'F' }, | |
], | |
annotation: 'Folks over 40', | |
}, | |
{ | |
list: [ | |
{ name: 'Alice', age: 25, gender: 'F' }, | |
{ name: 'Mohammed', age: 30, gender: 'M' }, | |
{ name: 'Charlie', age: 35, gender: 'M' }, | |
], | |
annotation: 'The Cis people under 40', | |
}, | |
{ | |
list: [{ name: 'Valentina', age: 24, gender: 'NB' }], | |
}, | |
]; | |
expect(grouped).toEqual(expected); | |
}); | |
it('should not return empty groups', () => { | |
type MegadethAlbums = { | |
name: string; | |
year: number; | |
genre: string; | |
rating: 1 | 2 | 3 | 4 | 5; | |
}; | |
const data: MegadethAlbums[] = [ | |
{ | |
name: 'Killing is My Business... and Business is Good!', | |
year: 1985, | |
genre: 'Thrash Metal', | |
rating: 4, | |
}, | |
{ | |
name: "Peace Sells... but Who's Buying?", | |
year: 1986, | |
genre: 'Thrash Metal', | |
rating: 5, | |
}, | |
{ name: 'So Far, So Good... So What!', year: 1988, genre: 'Thrash Metal', rating: 3 }, | |
{ name: 'Rust in Peace', year: 1990, genre: 'Thrash Metal', rating: 5 }, | |
{ name: 'Countdown to Extinction', year: 1992, genre: 'Groove Metal', rating: 5 }, | |
{ name: 'Youthanasia', year: 1994, genre: 'Groove Metal', rating: 4 }, | |
{ name: 'Cryptic Writings', year: 1997, genre: 'Heavy Metal', rating: 3 }, | |
{ name: 'Risk', year: 1999, genre: 'Heavy Metal', rating: 2 }, | |
{ name: 'The World Needs a Hero', year: 2001, genre: 'Heavy Metal', rating: 3 }, | |
{ name: 'The System Has Failed', year: 2004, genre: 'Heavy Metal', rating: 4 }, | |
{ name: 'United Abominations', year: 2007, genre: 'Heavy Metal', rating: 4 }, | |
{ name: 'Endgame', year: 2009, genre: 'Heavy Metal', rating: 5 }, | |
{ name: 'Th1rt3en', year: 2011, genre: 'Heavy Metal', rating: 4 }, | |
{ name: 'Super Collider', year: 2013, genre: 'Heavy Metal', rating: 3 }, | |
{ name: 'Dystopia', year: 2016, genre: 'Heavy Metal', rating: 5 }, | |
{ | |
name: 'The Sick, the Dying, and the Dead', | |
year: 2021, | |
genre: 'Heavy Metal', | |
rating: 5, | |
}, | |
]; | |
const grouped = groupByConfig<MegadethAlbums>( | |
{ | |
predicate: (album) => album.genre === 'Black Metal', | |
annotation: 'Black Metal Albums', | |
}, | |
{ | |
predicate: (album) => album.genre === 'Funeral Doom Metal', | |
annotation: 'Funeral Doom Metal Albums', | |
}, | |
{ | |
predicate: (album) => album.genre === 'Thrash Metal', | |
annotation: 'Thrash Albums', | |
}, | |
{ | |
predicate: (album) => album.year > 2010 && album.rating > 4, | |
annotation: 'The resurection in later years', | |
} | |
)(data); | |
const expected: Group<MegadethAlbums>[] = [ | |
{ | |
list: [ | |
{ | |
name: 'Killing is My Business... and Business is Good!', | |
year: 1985, | |
genre: 'Thrash Metal', | |
rating: 4, | |
}, | |
{ | |
name: "Peace Sells... but Who's Buying?", | |
year: 1986, | |
genre: 'Thrash Metal', | |
rating: 5, | |
}, | |
{ | |
name: 'So Far, So Good... So What!', | |
year: 1988, | |
genre: 'Thrash Metal', | |
rating: 3, | |
}, | |
{ | |
name: 'Rust in Peace', | |
year: 1990, | |
genre: 'Thrash Metal', | |
rating: 5, | |
}, | |
], | |
annotation: 'Thrash Albums', | |
}, | |
{ | |
list: [ | |
{ | |
name: 'Dystopia', | |
year: 2016, | |
genre: 'Heavy Metal', | |
rating: 5, | |
}, | |
{ | |
name: 'The Sick, the Dying, and the Dead', | |
year: 2021, | |
genre: 'Heavy Metal', | |
rating: 5, | |
}, | |
], | |
annotation: 'The resurection in later years', | |
}, | |
]; | |
const [thrash, resurrection /* ...rest */] = grouped; | |
expect([thrash, resurrection]).toEqual(expected); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* groupByConfig | |
* | |
* @description This function is a high order function that takes a GroupConfig objects as arguments | |
* and returns a function that takes a list of type ListType and returns a list of Group objects. | |
* | |
* @example | |
* ``` | |
* const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; | |
* const grouped = groupByConfig<number>( | |
* { predicate: (n) => n % 2 === 0, annotation: 'Even numbers' }, | |
* { predicate: (n) => n % 2 !== 0, annotation: 'Odd numbers' }, | |
* )(numbers); | |
* | |
* // grouped = [ | |
* // { list: [2, 4, 6, 8, 10], annotation: 'Even numbers' }, | |
* // { list: [1, 3, 5, 7, 9], annotation: 'Odd numbers' }, | |
* // ]; | |
* ``` | |
* | |
* groupByConfig will pass the remaining elements of the list to the next predicate, so the order | |
* of the predicates is important. For example, | |
* @example | |
* ``` | |
* const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; | |
* const grouped = groupByConfig<number>( | |
* { predicate: (n) => n % 2 === 0, annotation: 'Even numbers' }, | |
* { predicate: (n) => n % 3 === 0, annotation: 'Divisible by 3' }, | |
* { predicate: (n) => n % 2 !== 0, annotation: 'Odd numbers' }, | |
* )(numbers); | |
* | |
* // grouped = [ | |
* // { list: [2, 4, 6, 8, 10], annotation: 'Even numbers' }, | |
* // { list: [3, 9], annotation: 'Divisible by 3' }, | |
* // { list: [1, 5, 7], annotation: 'Odd numbers' }, | |
* // ]; | |
* ``` | |
*/ | |
export type GroupConfig<T> = { | |
predicate: (arg: T) => boolean; | |
annotation?: string; | |
}; | |
// exported for testing | |
export type Group<T> = { | |
list: T[]; | |
annotation?: string; | |
}; | |
export const groupByConfig = | |
<ListType>(...configs: GroupConfig<ListType>[]) => | |
(list: ListType[]) => | |
configs.reduce<Group<ListType>[]>( | |
(acc, config): Group<ListType>[] => { | |
const last = acc.pop(); | |
if (Array.isArray(last) && last.length === 0) { | |
return acc; | |
} | |
const { predicate, annotation } = config; | |
const [left, right] = separate<ListType>(last?.list ?? [], predicate); | |
return [ | |
...acc, | |
...(left.length !== 0 ? [{ list: left, annotation }] : []), | |
...(right.length !== 0 ? [{ list: right }] : []), | |
]; | |
}, | |
[{ list }] | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment