Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/babel-plugin-factories.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"patronum/spread",
"patronum/status",
"patronum/throttle",
"patronum/time"
"patronum/time",
"patronum/xor"
],
"mapping": {
"and": "and",
Expand Down Expand Up @@ -56,6 +57,7 @@
"spread": "spread",
"status": "status",
"throttle": "throttle",
"time": "time"
"time": "time",
"xor": "xor"
}
}
}
19 changes: 19 additions & 0 deletions src/xor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { combine, Store } from 'effector';

export function xor(...stores: Array<Store<any>>): Store<boolean> {
return combine(
stores,
(values) => {
let trueCount = 0;

for (const value of values) {
if (value) {
trueCount += 1;
}
}

return trueCount === 1;
},
{ skipVoid: false },
) as Store<boolean>;
}
54 changes: 54 additions & 0 deletions src/xor/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: xor
slug: xor
description: Logical XOR for multiple stores
group: combination
---

```ts
import { xor } from 'patronum';
// or
import { xor } from 'patronum/xor';
```

### Motivation

Combines multiple stores, returning `true` if exactly one of them is truthy.

### Formulae

```ts
$result = xor($first, $second);
```

- `$result` will be `true`, if exactly one of the stores is truthy
- `$result` will be `false`, if all stores are falsy or more than one store is truthy

### Arguments

- `stores: Array<Store<any>>` — Any number of stores to check through XOR

### Returns

- `result: Store<boolean>` — Store with the result of the XOR operation

### Example

#### Basic usage

```ts
import { createStore } from 'effector';
import { xor } from 'patronum/xor';

const $isOnline = createStore(true);
const $isProcessing = createStore(false);

const $isDisabled = xor($isOnline, $isProcessing);
console.assert(true === $isDisabled.getState());
// $isDisabled === true, because $isOnline === true and $isProcessing === false

const $hasError = createStore(true);
const $result = xor($isOnline, $isProcessing, $hasError);
console.assert(false === $result.getState());
// $result === false, because multiple stores are truthy ($isOnline and $hasError)
```
39 changes: 39 additions & 0 deletions src/xor/xor.fork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createStore, createEvent, fork, allSettled } from 'effector';
import { xor } from './index';

test('xor in forked scope', async () => {
const $a = createStore(false);
const $b = createStore(false);
const $c = createStore(false);
const changeA = createEvent<boolean>();
const changeB = createEvent<boolean>();
const changeC = createEvent<boolean>();

$a.on(changeA, (_, value) => value);
$b.on(changeB, (_, value) => value);
$c.on(changeC, (_, value) => value);

const $result = xor($a, $b, $c);

const scope = fork();

expect(scope.getState($result)).toBe(false);

await allSettled(changeA, { scope, params: true });
expect(scope.getState($result)).toBe(true);

await allSettled(changeB, { scope, params: true });
expect(scope.getState($result)).toBe(false);

await allSettled(changeB, { scope, params: false });
expect(scope.getState($result)).toBe(true);

await allSettled(changeC, { scope, params: true });
expect(scope.getState($result)).toBe(false);

await allSettled(changeA, { scope, params: false });
expect(scope.getState($result)).toBe(true);

await allSettled(changeC, { scope, params: false });
expect(scope.getState($result)).toBe(false);
});
66 changes: 66 additions & 0 deletions src/xor/xor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createStore, createEvent, fork, allSettled } from 'effector';
import { xor } from './index';

describe('xor', () => {
test('returns true when exactly one store is truthy', () => {
const $a = createStore(true);
const $b = createStore(false);
const $c = createStore(false);

const $result = xor($a, $b, $c);

expect($result.getState()).toBe(true);
});

test('returns false when multiple stores are truthy', () => {
const $a = createStore(true);
const $b = createStore(true);
const $c = createStore(false);

const $result = xor($a, $b, $c);

expect($result.getState()).toBe(false);
});

test('returns false when no stores are truthy', () => {
const $a = createStore(false);
const $b = createStore(false);
const $c = createStore(false);

const $result = xor($a, $b, $c);

expect($result.getState()).toBe(false);
});

test('updates correctly when source store changes', () => {
const $a = createStore(true);
const $b = createStore(false);
const changeA = createEvent<boolean>();
const changeB = createEvent<boolean>();
$a.on(changeA, (_, value) => value);
$b.on(changeB, (_, value) => value);

const $result = xor($a, $b);

expect($result.getState()).toBe(true);

changeA(false);
expect($result.getState()).toBe(false);

changeB(true);
expect($result.getState()).toBe(true);

changeA(true);
expect($result.getState()).toBe(false);
});

test('accepts non-boolean values and coerces them', () => {
const $a = createStore(0);
const $b = createStore('');
const $c = createStore(1);

const $result = xor($a, $b, $c);

expect($result.getState()).toBe(true);
});
});
43 changes: 43 additions & 0 deletions test-typings/xor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expectType } from 'tsd';
import {
Store,
createStore,
createDomain,
createEvent,
createEffect,
} from 'effector';
import { xor } from '../dist/xor';

// Returns always store with boolean
{
const $a = createStore(1);
const $b = createStore('a');
const $c = createStore(true);
const $d = createStore([]);
const $e = createStore({});
const $f = createStore(() => {});
const $g = createStore(Symbol.for('demo'));

expectType<Store<boolean>>(xor($a, $b, $c, $d, $e, $f, $g));
}

// Doesn't allow to pass non-store as argument
{
// @ts-expect-error
xor(createDomain());

// @ts-expect-error
xor(createEvent());

// @ts-expect-error
xor(createEffect());
}

// Allows to receive derived stores
{
const $source = createStore(1);
const $derived = $source.map((i) => i * 100);
const fx = createEffect();

expectType<Store<boolean>>(xor($derived, fx.pending));
}
Loading