Skip to content

Commit

Permalink
added groupby with transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-surinin committed Nov 20, 2019
1 parent a924937 commit dd91d2f
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 54 deletions.
58 changes: 51 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ _declarative-js_ is modern JavaScript library, that helps to:
# Why _declarative-js_?
- performance [(link to benchmarks)](https://github.com/pavel-surinin/performance-bechmark/blob/master/output.md)
- ability to use with built in api (js array)
- it is writen in `typescript`. All functions provides great type inferance
- it is written in `typescript`. All functions provides great type inference
- declarative code instead of imperative
- reduces boilerplate code providing performant and tested solutions
- comprehensive documentation [(link)](https://pavel-surinin.github.io/declarativejs/#/)
Expand Down Expand Up @@ -75,7 +75,7 @@ const data = [
data.reduce(toObject(
movie => movie.genre,
movie => [movie.title],
(movie1, moveie2) => movie1.concat(movie2)),
(movie1, movie2) => movie1.concat(movie2)),
{}
)
// {
Expand All @@ -90,8 +90,10 @@ Groups by key resolved from callback to map where key is `string` and value is a
Custom implementation of Map can be passed as a second parameter. It must implement interface [MethodMap](#methodmap).
Provided implementations can be imported from same namespace `Reducer.ImmutableMap` or `Reducer.Map`

_performance benchmark_: [link](https://github.com/pavel-surinin/performance-bechmark/blob/master/output.md#reducergroupby)

*group by original values example*

_performance benchmark_: [link](https://github.com/pavel-surinin/performance-bechmark/blob/master/output.md#reducergroupby)
```javascript
import { Reducers } from 'declarative-js'
import groupBy = Reducers.groupBy
Expand All @@ -107,6 +109,48 @@ const data = [
data.reduce(groupBy(movie => move.genre), Map())
data.reduce(groupBy('genre'), Map())

// {
// 'scy-fy': [
// { title: 'Predator', genre: 'scy-fy' },
// { title: 'Predator 2', genre: 'scy-fy' },
// { title: 'Alien vs Predator', genre: 'scy-fy' }
// ],
// 'cartoon': [
// { title: 'Tom & Jerry', genre: 'cartoon' }
// ],
// }
```
*group by transformed values example*

```javascript
import { Reducers } from 'declarative-js'
import groupBy = Reducers.groupBy
import Map = Reducers.Map

const data = [
{ title: 'Predator', genre: 'sci-fi' },
{ title: 'Predator 2', genre: 'sci-fi'},
{ title: 'Alien vs Predator', genre: 'sci-fi' },
{ title: 'Tom & Jerry', genre: 'cartoon' }
]

data.reduce(
groupBy(
movie => move.genre,
movie => movie.title
),
Map(),
)
data.reduce(groupBy('genre', movie => movie.title), Map())

// {
// 'scy-fy': [
// 'Predator',
// 'Predator2',
// 'Alien vs Predator'
// ],
// 'cartoon': [ 'Tom & Jerry' ],
// }
```

### flat
Expand Down Expand Up @@ -331,7 +375,7 @@ reduced2.values() // [11, 12]

### toMergedObject
Reduces array of objects to one object
There is three predifined merge strategies
There is three predefined merge strategies

```javascript
import { Reducer } from 'declarative-js'
Expand Down Expand Up @@ -366,8 +410,8 @@ import MergeStrategy = Reducers.MergeStrategy
[ {e: 1}, {e: 2}, {c: 3} ].reduce(toMergedObject(MergeStrategy.UNIQUE), {}) // ERROR
```

Since MergeStrategy is just a predicate function with delaration: `(aggregatorValue: T, currentValue: T, key: string) => boolean`
Developer can define its own predicate to avoid object raversing and check, are all properties equal.
Since MergeStrategy is just a predicate function with declaration: `(aggregatorValue: T, currentValue: T, key: string) => boolean`
Developer can define its own predicate to avoid object traversing and check, are all properties equal.

```javascript
import { Reducers } from 'declarative-js'
Expand All @@ -387,7 +431,7 @@ import toMergedObject = Reducers.toMergedObject
}
},
{
alienVspredator: {
alienVsPredator: {
title: 'Alien vs Predator',
genre: 'scy-fy'
}
Expand Down
26 changes: 17 additions & 9 deletions __tests__/array/reduce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,15 +285,23 @@ describe('Reducer', () => {
expect(reduced.keys()).toMatchObject(['Mike', 'John'])
expect(reduced.values()).toMatchObject([[{ name: 'Mike' }], [{ name: 'John' }, { name: 'John' }]])
})
it.skip('group by and modify elements by callback', () => {
// const array = [{ name: 'Mike' }, { name: 'John' }, { name: 'John' }]
// const reduced = array.reduce(
// Reducer.groupBy(x => x.name),
// Reducer.Map()
// )
// expect(reduced.keys()).toMatchObject(['Mike', 'John'])
// expect(reduced.values()).toMatchObject([['Mike'], ['John', 'John']])
// reduced.values().map(x => x.m)
it('group by callback and modify elements by callback', () => {
const array = [{ name: 'Mike' }, { name: 'John' }, { name: 'John' }]
const reduced = array.reduce(
Reducer.groupBy(x => x.name, x => x.name),
Reducer.Map()
)
expect(reduced.keys()).toMatchObject(['Mike', 'John'])
expect(reduced.values()).toMatchObject([['Mike'], ['John', 'John']])
})
it('group by key and modify elements by callback', () => {
const array = [{ name: 'Mike' }, { name: 'John' }, { name: 'John' }]
const reduced = array.reduce(
Reducer.groupBy('name', x => x.name),
Reducer.Map()
)
expect(reduced.keys()).toMatchObject(['Mike', 'John'])
expect(reduced.values()).toMatchObject([['Mike'], ['John', 'John']])
})
});
it('should flat from 2d array to simple array', () => {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"to object",
"collect",
"zip",
"unzip",
"partition",
"partition by",
"functional",
Expand Down Expand Up @@ -77,4 +78,4 @@
"publish:major": "npm run build && npm version major && npm run copy && npm publish target/out-es5",
"coveralls": "cat target/coverage/lcov.info | coveralls"
}
}
}
161 changes: 124 additions & 37 deletions src/array/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export namespace Reducer {
return Object.defineProperty(
object,
'immutable',
{
value: true,
enumerable: false
}
{ value: true, enumerable: false }
) as StringMap<T>
}

Expand All @@ -55,20 +52,30 @@ export namespace Reducer {
* Reducer.ImmutableMap()
* Or own implementation of {@link MethodMap}
* @param {string} key objects key to resolve value,to group by it
* @throws {Error} if resolved key from callback is not a string
* @example
* [
* { title: 'Predator', genre: 'scy-fy },
* { title: 'Predator 2', genre: 'scy-fy},
* { title: 'Alien vs Predator', genre: 'scy-fy },
* { title: 'Tom & Jerry', genre: 'cartoon },
* { title: 'Predator', genre: 'scy-fy' },
* { title: 'Predator 2', genre: 'scy-fy'},
* { title: 'Alien vs Predator', genre: 'scy-fy' },
* { title: 'Tom & Jerry', genre: 'cartoon' },
* ]
* .reduce(groupBy('genre'), Reducer.Map())
* // {
* // 'scy-fy': [
* // { title: 'Predator', genre: 'scy-fy' },
* // { title: 'Predator 2', genre: 'scy-fy' },
* // { title: 'Alien vs Predator', genre: 'scy-fy' }
* // ],
* // 'cartoon': [
* // { title: 'Tom & Jerry', genre: 'cartoon' }
* // ],
* // }
*/
export function groupBy<T, K extends keyof T>(key: K):
(agr: MethodMap<T[]>, value: T, index: number, array: T[]) => MethodMap<T[]>
/**
* Function to be used in {@link Array.prototype.reduce} as a callback to group by provided key.
* Groups an array by key resolved from callback.
* Function to be used in {@link Array.prototype.reduce} as a callback to group by provided function.
* As second parameter in reduce function need to pass
* Reducer.Map()
* Reducer.ImmutableMap()
Expand All @@ -83,44 +90,124 @@ export namespace Reducer {
* { title: 'Tom & Jerry', genre: 'cartoon },
* ]
* .reduce(groupBy(movie => movie.genre), Reducer.Map())
* // {
* // 'scy-fy': [
* // { title: 'Predator', genre: 'scy-fy' },
* // { title: 'Predator 2', genre: 'scy-fy' },
* // { title: 'Alien vs Predator', genre: 'scy-fy' }
* // ],
* // 'cartoon': [
* // { title: 'Tom & Jerry', genre: 'cartoon' }
* // ],
* // }
*/
export function groupBy<T>(getKey: KeyGetter<T>):
(agr: MethodMap<T[]>, value: T, index: number, array: T[]) => MethodMap<T[]>

/**
* Groups an array by key resolved from callback and transform value to put in new grouped array.
* Function to be used in {@link Array.prototype.reduce} as a callback to group by provided key.
* As second parameter in reduce function need to pass
* {@link Reducer.Map()}, {@link Reducer.ImmutableMap()} or own implementation of {@link MethodMap}
*
* @export
* @template T type of element in array
* @template TR type of element in grouped array
* @param {KeyGetter<T>} getKey function to get key, output must be a string
* by this key an array will be grouped
* @param {Getter<T, TR>} transformer function to transform array element in grouped array
* @returns {(agr: MethodMap<TR[]>, value: T, index: number, array: T[]) => MethodMap<TR[]>}
* function to use in Array.reduce
* @throws {Error} if resolved key from callback is not a string
* @example
* [
* { title: 'Predator', genre: 'scy-fy },
* { title: 'Predator 2', genre: 'scy-fy},
* { title: 'Alien vs Predator', genre: 'scy-fy },
* { title: 'Tom & Jerry', genre: 'cartoon },
* ]
* .reduce(groupBy(movie => movie.genre, movie -> movie.title), Reducer.Map())
* // {
* // 'scy-fy': [
* // 'Predator',
* // 'Predator2',
* // 'Alien vs Predator'
* // ],
* // 'cartoon': [ 'Tom & Jerry' ],
* // }
*/
export function groupBy<T, TR>(getKey: KeyGetter<T>, transformer: Getter<T, TR>):
(agr: MethodMap<T[]>, value: T, index: number, array: T[]) => MethodMap<T[]>
(agr: MethodMap<TR[]>, value: T, index: number, array: T[]) => MethodMap<TR[]>

export function groupBy<T, K extends keyof T>(getKey: KeyGetter<T> | K) {
if (typeof getKey === 'string') {
const key = getKey
return function (agr: MethodMap<T[]>, value: T, index: number, array: T[]) {
const derivedKey = value[key]
if (typeof derivedKey === 'string') {
const derivedValue = agr.get(derivedKey)
if (derivedValue) {
derivedValue.push(value)
/**
* Groups an array by key and transform value to put in new grouped array.
* Function to be used in {@link Array.prototype.reduce} as a callback to group by provided key.
* As second parameter in reduce function need to pass
* {@link Reducer.Map()}, {@link Reducer.ImmutableMap()} or own implementation of {@link MethodMap}
*
* @export
* @template T type of element in array
* @template TR type of element in grouped array
* @param {string} key of an element in array object to group by it
* @param {Getter<T, TR>} transformer function to transform array element in grouped array
* @returns {(agr: MethodMap<TR[]>, value: T, index: number, array: T[]) => MethodMap<TR[]>}
* function to use in Array.reduce
* @example
* [
* { title: 'Predator', genre: 'scy-fy },
* { title: 'Predator 2', genre: 'scy-fy},
* { title: 'Alien vs Predator', genre: 'scy-fy },
* { title: 'Tom & Jerry', genre: 'cartoon },
* ]
* .reduce(groupBy('genre', movie -> movie.title), Reducer.Map())
* // {
* // 'scy-fy': [
* // 'Predator',
* // 'Predator2',
* // 'Alien vs Predator'
* // ],
* // 'cartoon': [ 'Tom & Jerry' ],
* // }
*/

export function groupBy<T, TR, K extends keyof T>(key: K, transformer: Getter<T, TR>):
(agr: MethodMap<TR[]>, value: T, index: number, array: T[]) => MethodMap<TR[]>

export function groupBy<T, K extends keyof T, TR>(
getKey: KeyGetter<T> | K,
transformer: Getter<T, TR> = x => x as any as TR
) {
switch (typeof getKey) {
case 'string':
const key = getKey
return function (agr: MethodMap<TR[]>, value: T, index: number, array: T[]) {
const derivedKey = value[key]
if (typeof derivedKey === 'string') {
const derivedValue = agr.get(derivedKey)
if (derivedValue) {
derivedValue.push(transformer(value))
} else {
agr.put(derivedKey, [transformer(value)])
}
return isLastElement(array, index) ? finalizeMap(agr) : agr
}
// tslint:disable-next-line:max-line-length
throw new Error('Value of "' + key + '" in groupBy ' + ' must be string, instead get: ' + typeof value[key])
}
case 'function':
return function (agr: MethodMap<TR[]>, value: T, index: number, array: T[]) {
const key = valid(getKey(value))
const extractedValue = agr.get(key)
if (extractedValue !== void 0) {
extractedValue.push(transformer(value))
} else {
agr.put(derivedKey, [value])
agr.put(key, [transformer(value)])
}
return isLastElement(array, index) ? finalizeMap(agr) : agr
}
default:
// tslint:disable-next-line:max-line-length
throw new Error('Value of "' + key + '" in groupBy ' + ' must be string, instead get: ' + typeof value[key])
}
} else if (typeof getKey === 'function') {
return function (agr: MethodMap<T[]>, value: T, index: number, array: T[]) {
const key = valid(getKey(value))
const extractedValue = agr.get(key)
if (extractedValue !== void 0) {
extractedValue.push(value)
} else {
agr.put(key, [value])
}
return isLastElement(array, index) ? finalizeMap(agr) : agr
}
} else {
// tslint:disable-next-line:max-line-length
throw new Error(`Reducer.groupBy function accepts as a paramter string or callback, instead got ${typeof getKey}`)
throw new Error(`Reducer.groupBy function accepts as a paramter string or callback, instead got ${typeof getKey}`)
}
}

Expand Down

0 comments on commit dd91d2f

Please sign in to comment.