Skip to content

Instantly share code, notes, and snippets.

@nathanstilwell
Last active April 19, 2024 20:58
Show Gist options
  • Save nathanstilwell/3fc4b7a27441efb1725f937e633c9856 to your computer and use it in GitHub Desktop.
Save nathanstilwell/3fc4b7a27441efb1725f937e633c9856 to your computer and use it in GitHub Desktop.
Something I was toying with that may not be a good idea.
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);
});
});
/**
* 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