Skip to content

Commit

Permalink
Merge pull request #2 from s-taylor/version-2
Browse files Browse the repository at this point in the history
Rework completely to use new Set approach
  • Loading branch information
s-taylor authored Jun 20, 2019
2 parents a4fa25c + 16aa776 commit 852b55b
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 16 deletions.
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ A function to filter unique (you-neek) values

## Why?

It always felt natural to me to use filter to find unique values in an array. Filter is meant to take an array, and remove unwanted results which is exactly what this does.
It always felt natural to me to use `.filter` to find unique values in an array. Filter is meant to take an array, and remove unwanted results which is exactly what you are doing when getting unique values from an array. You iterate through an array, and remove any duplicate values you find, and then return the result.

A neat alternative is with ES6 you can do this...
A neat trick with ES6 is you can get unique values using...

```js
const unique = arr => [...new Set(arr)];
```

However to me, `arr.filter(unique)` is better than `unique(arr)`, especially if you're chaining to a `.map`.
However to me, `arr.filter(unique())` is better than `unique(arr)`, especially if you're chaining to a `.map`, `.reduce` or other `.filter`.

## How to use it?

Expand All @@ -23,7 +23,8 @@ const unique = require('youneek');

const arr = [1,2,3,4,1,3,5,8,9,9];

const result = arr.filter(unique);
/* Make sure you call unique! See why below */
const result = arr.filter(unique());

// result === [1, 2, 3, 4, 5, 8, 9];
```
Expand All @@ -32,11 +33,38 @@ const result = arr.filter(unique);

Though to be honest, you could copy and paste this. It's really small and I won't begrudge you for doing that, but maybe give me a star?

```js
const unique = () => {
let cache;
return (elem, index, array) => {
if (!cache) cache = new Set(array);
return cache.delete(elem);
};
};
```
## Older browsers

This package uses `new Set`. If you are on an older browser you may need a polyfill.

* [Set support](https://kangax.github.io/compat-table/es6/#test-Set)
* [Polyfill with core-js](https://github.com/zloirock/core-js)

## How it works

The original version of this package did not require use of `Set` and was usable via `.filter(unique)` rather than `filter(unique())` which was nicer. It looked like this...

```js
function unique(elem, index, array) {
for (var i = 0; i < index; i++) {
if (array[i] === elem) return false;
}
return true;
};
```
This works by looping through the array and it checks each element against all the elements prior to it, to see if the same value exists. If it finds the same value earlier in the array it will return `false` and filter it out.
The problem with this approach is you end up rechecking the same values over and over. It is smart enough to abort as soon as it sees a duplication but on larger arrays performance will suffer.
So I wanted to find a solution that is performant. The unfortunate problem with `.filter` is there isn't any way to see the result as you iterate, so there isn't any way to check if you already found the value earlier in the iteration. You can't just check the results you have generated so far to see if the current value exists in there.
The solution to this is to use a closure which on the first element will use the `[...new Set(arr)]` trick to find all the unique values and store in `cache`. Then as it iterates it simply checks each value against the `cache`. It does this using `.delete` which means the first time a value is found, it will return `true` (for a successful deletion) since every element exists once in the cache. On subsequent checks for the same value (i.e. duplicates) it will return `false` since that value has been deleted (the deletion fails).
16 changes: 10 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
function unique(elem, index, array) {
for (var i = 0; i < index; i++) {
if (array[i] === elem) return false;
}
return true;
"use strict";

var unique = function unique() {
var cache;
return function (elem, index, array) {
/* Note: `new Set` may require @babel/polyfill */
if (!cache) cache = new Set(array);
return cache.delete(elem);
};
};

module.exports = unique;
module.exports = unique;
19 changes: 14 additions & 5 deletions test/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const test = require('ava');
const unique = require('../index');

test('must correctly filter unique numbers', (t) => {
const arr = [1,2,3,4,1,3,5,8,9,9];
const arr = [1, 2, 3, 4, 1, 3, 5, 8, 9, 9];

const result = arr.filter(unique);
const result = arr.filter(unique());

const expected = [1, 2, 3, 4, 5, 8, 9];
t.deepEqual(result, expected);
Expand All @@ -13,16 +13,25 @@ test('must correctly filter unique numbers', (t) => {
test('must correctly filter unique strings', (t) => {
const arr = ['Mark', 'Steve', 'Bernie', 'Mark', 'Harry', 'Joe', 'Harry', 'Mark'];

const result = arr.filter(unique);
const result = arr.filter(unique());

const expected = ['Mark', 'Steve', 'Bernie', 'Harry', 'Joe'];
t.deepEqual(result, expected);
});

test('must correctly filter a mixture', (t) => {
const arr = ['a', 2, 3, 4, 1, 3, 'a', 8, 'b', 2];

const result = arr.filter(unique());

const expected = ['a', 2, 3, 4, 1, 8, 'b'];
t.deepEqual(result, expected);
});

test('must not mess with types that cannot be compared', (t) => {
const arr = [{}, [1,2,3], { awesome: true }, []];
const arr = [{}, [1, 2, 3], { awesome: true }, []];

const result = arr.filter(unique);
const result = arr.filter(unique());

t.deepEqual(result, arr);
});

0 comments on commit 852b55b

Please sign in to comment.