Skip to content

Commit

Permalink
Merge pull request #10 from BrianMitchL/custom-timeouts
Browse files Browse the repository at this point in the history
Add timeout option
  • Loading branch information
BrianMitchL committed Sep 27, 2020
2 parents 3394810 + 32e2534 commit 127fb0f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 32 deletions.
63 changes: 41 additions & 22 deletions README.md
Expand Up @@ -17,13 +17,14 @@ npm i gift-exchange
The library ships CommonJS, ES module, and UMD builds. The UMD build makes the
library available with the `GiftExchange` name.

`gift-exchange` exports two functions (`calculate` and `calculateSync`) and an
Error, `DerangementError`.
`gift-exchange` exports two functions (`calculateSync` and `calculate`
(deprecated)) and an Error, `DerangementError`.

A `Person` array is always required. A `Person` must have a unique `name` and
optionally a `group`. A `Person` cannot be matched with another person in the
same `group` nor with themselves. A mix of people that both have and do not
have a `group` is supported.
have a `group` is supported. Additional exclusion logic can be configured with
[Exclusions](#exclusions).

```typescript
import { Person } from 'gift-exchange';
Expand All @@ -39,15 +40,30 @@ const people: Person[] = [
];
```

### `calculate`
### `calculateSync`

```typescript
function calculateSync(
people: Person[],
exclusions?: Exclusion[]
): Person[];
// or
function calculateSync(
people: Person[],
// timeout in ms
options?: { exclusions?: Exclusion[], timeout?: number }
): Person[];
```

This returns a Promise that resolves with a new array of people or
rejects with a `DerangementError`. This error is thrown if the matching
algorithm fails to find a valid match after 1 second, indicating that an
impossible combination of people and exclusions was provided.
This returns a new `Person` array or throws a `DerangementError` if
the matching algorithm fails to find a valid match after 1 second (or custom
timeout, if provided), indicating that an impossible combination of people and
exclusions was provided. If given an impossible configuration or one with few
possible matches, and many people, this will block the thread. To avoid this,
it is recommended to run the script in a WebWorker.

```typescript
import { calculate, Person } from 'gift-exchange';
import { calculateSync, Person } from 'gift-exchange';

const people: Person[] = [
{
Expand All @@ -58,23 +74,29 @@ const people: Person[] = [
}
];

calculate(people).then(matches => {
try {
const matches = calculateSync(people);
const pairs: { from: string; to: string }[] = people.map((person, i) => ({
from: person.name,
to: matches[i].name
}));
console.table(pairs);
});
} catch (e) {
console.error(e);
}
```

### `calculateSync`
### `calculate` (deprecated)

**Using a `Promise` is straightforward and still thread blocking, so this will
be removed in the next major version.**

This returns a new array of people or throws a `DerangementError` if
the matching algorithm fails to find a valid match after 1 second, indicating
that an impossible combination of people and exclusions was provided.
This function takes the same arguments as `calculateSync`, but returns a
`Promise` resolved with the `Person` array, or rejected with the
`DerangementError`.

```typescript
import { calculateSync, Person } from 'gift-exchange';
import { calculate, Person } from 'gift-exchange';

const people: Person[] = [
{
Expand All @@ -85,21 +107,18 @@ const people: Person[] = [
}
];

try {
const matches = calculateSync(people);
calculate(people).then(matches => {
const pairs: { from: string; to: string }[] = people.map((person, i) => ({
from: person.name,
to: matches[i].name
}));
console.table(pairs);
} catch (e) {
console.error(e);
}
});
```

### Exclusions

The `calculate` and `calculateSync` functions can also be called with a second
The `calculateSync` and `calculate` functions can also be called with a second
argument `exclusions`. This builds upon the concept that no person can match
another in the same group.

Expand Down
46 changes: 36 additions & 10 deletions src/derange.ts
Expand Up @@ -33,13 +33,29 @@ export const validateMatches: ValidateMatches = (
});
};

export const derange = (
export function derange(people: Person[], exclusions?: Exclusion[]): Person[];
export function derange(
people: Person[],
exclusions: Exclusion[] = []
): Person[] => {
options?: { exclusions?: Exclusion[]; timeout?: number }
): Person[];

export function derange(
people: Person[],
exclusionsOrOptions?:
| { exclusions?: Exclusion[]; timeout?: number }
| Exclusion[]
): Person[] {
if (people.length < 2) {
return people.slice(0);
}
let exclusions: Exclusion[];
let timeout = 1000;
if (Array.isArray(exclusionsOrOptions)) {
exclusions = exclusionsOrOptions;
} else {
exclusions = exclusionsOrOptions?.exclusions ?? [];
timeout = exclusionsOrOptions?.timeout ?? 1000;
}

let buffer1: Person[] = [];
let buffer2: Person[] = [];
Expand All @@ -57,7 +73,7 @@ export const derange = (
const startTime = Date.now();
const testDerangement: ValidateMatches = (...args): boolean => {
// prevent infinite loops when no combination is found
if (Date.now() - startTime > 1e3)
if (Date.now() - startTime > timeout)
throw new DerangementError('No derangement found');
return validateMatches(...args);
};
Expand All @@ -72,17 +88,27 @@ export const derange = (
const personIndex = buffer1.findIndex(match => match.name === p.name);
return buffer2[personIndex];
});
};
}

export const calculate = (
export function calculate(people: Person[], exclusions?: Exclusion[]): Promise<Person[]>;
export function calculate(
people: Person[],
options?: { exclusions?: Exclusion[]; timeout?: number }
): Promise<Person[]>;
/**
* @deprecated
* This is thread blocking, even when in wrapped in a Promise
* A better non-blocking approach would be to wrap the call in a WebWorker
*/
export function calculate(
people: Person[],
exclusions: Exclusion[] = []
): Promise<Person[]> => {
exclusionsOrOptions?: any
): Promise<Person[]> {
return new Promise((resolve, reject) => {
try {
resolve(derange(people, exclusions));
resolve(derange(people, exclusionsOrOptions));
} catch (e) {
reject(e);
}
});
};
}
41 changes: 41 additions & 0 deletions test/derange.test.ts
Expand Up @@ -349,6 +349,33 @@ describe('derange', () => {
expect(() => derange(input, exclusions)).not.toBeValidDerangement(input);
});

it('throws an error when an impossible combination is received with a custom timeout', () => {
const input = personArrayOfLength(3);
const exclusions: Exclusion[] = [
{
type: 'name',
subject: '2',
excludedType: 'name',
excludedSubject: '1'
},
{
type: 'name',
subject: '1',
excludedType: 'name',
excludedSubject: '2'
}
];

const start = Date.now();

expect(() => derange(input, { exclusions, timeout: 10 })).toThrowError();
expect(() =>
derange(input, { exclusions, timeout: 10 })
).not.toBeValidDerangement(input);

expect(Date.now() - start).toBeGreaterThanOrEqual(20);
});

it('deranges with a name exclusion', () => {
const input = personArrayOfLength(3);
const exclusions: Exclusion[] = [
Expand All @@ -363,6 +390,20 @@ describe('derange', () => {
expect(derange(input, exclusions)).toBeValidDerangement(input);
});

it('deranges with a name exclusion using object argument syntax', () => {
const input = personArrayOfLength(3);
const exclusions: Exclusion[] = [
{
type: 'name',
subject: '2',
excludedType: 'name',
excludedSubject: '1'
}
];

expect(derange(input, { exclusions })).toBeValidDerangement(input);
});

it('deranges with a group exclusion', () => {
const input = personArrayOfLength(3);
input[0].group = 'a';
Expand Down

0 comments on commit 127fb0f

Please sign in to comment.