Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement includes operator #342

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions src/babel-plugin-factories.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"patronum/either",
"patronum/empty",
"patronum/equals",
"patronum/includes",
"patronum/every",
"patronum/format",
"patronum/in-flight",
Expand Down Expand Up @@ -37,6 +38,7 @@
"either": "either",
"empty": "empty",
"equals": "equals",
"includes": "includes",
"every": "every",
"format": "format",
"inFlight": "in-flight",
Expand Down
86 changes: 86 additions & 0 deletions src/includes/includes.fork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { allSettled, createEvent, createStore, fork } from 'effector';
import { includes } from './index';

it('should return true if number is included in array', async () => {
const $array = createStore([1, 2, 3]);
const $result = includes($array, 2);

const scope = fork();
expect(scope.getState($result)).toBe(true);
});

it('should return false if number is not included in array', async () => {
const $array = createStore([1, 2, 3]);
const $result = includes($array, 4);

const scope = fork();
expect(scope.getState($result)).toBe(false);
});

it('should return true if store number is included in array', async () => {
const $array = createStore([1, 2, 3]);
const $findInArray = createStore(2);
const $result = includes($array, $findInArray);

const scope = fork();
expect(scope.getState($result)).toBe(true);
});

it('should return true if string is included in string store', async () => {
const $string = createStore('Hello world!');
const $result = includes($string, 'Hello');

const scope = fork();
expect(scope.getState($result)).toBe(true);
});

it('should return false if string is not included in string store', async () => {
const $string = createStore('Hello world!');
const $result = includes($string, 'Goodbye');

const scope = fork();
expect(scope.getState($result)).toBe(false);
});

it('should return true if store string is included in string store', async () => {
const $string = createStore('Hello world!');
const $findInString = createStore('Hello');
const $result = includes($string, $findInString);

const scope = fork();
expect(scope.getState($result)).toBe(true);
});

it('should update result if array store changes', async () => {
const updateArray = createEvent<number[]>();
const $array = createStore([1, 2, 3]).on(updateArray, (_, newArray) => newArray);
const $result = includes($array, 4);

const scope = fork();
expect(scope.getState($result)).toBe(false);

await allSettled(updateArray, { scope, params: [1, 2, 3, 4] });
expect(scope.getState($result)).toBe(true);
});

it('should update result if string store changes', async () => {
const changeString = createEvent<string>();
const $array = createStore('Hello world!');
const $findInArray = createStore('Goodbye').on(changeString, (_, newString) => newString);
const $result = includes($array, $findInArray);

const scope = fork();
expect(scope.getState($result)).toBe(false);

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

it('should throw an error if first argument is a number instead of array or string', () => {
const $number = createStore(5);
const $findNumber = createStore(5);

expect(() => includes(($number as any), $findNumber)).toThrowError(
'first argument should be an unit of array or string'
);
});
82 changes: 82 additions & 0 deletions src/includes/includes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createEvent, createStore } from 'effector';
import { includes } from './index';
import { not } from '../not';

describe('includes', () => {
it('should return true if number is included in array', () => {
const $array = createStore([1, 2, 3]);
const $result = includes($array, 2);

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

it('should return false if number is not included in array', () => {
const $array = createStore([1, 2, 3]);
const $result = includes($array, 4);

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

it('should return true if store number is included in array', () => {
const $array = createStore([1, 2, 3]);
const $findInArray = createStore(2);
const $result = includes($array, $findInArray);

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

it('should return true if store number is not included in array', () => {
const $array = createStore([1, 2, 3]);
const $findInArray = createStore(4);
const $result = includes($array, $findInArray);

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

it('should return true if string is included in string store', () => {
const $string = createStore('Hello world!');
const $result = includes($string, 'Hello');

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

it('should return false if string is not included in string store', () => {
const $string = createStore('Hello world!');
const $result = includes($string, 'Goodbye');

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

it('should return true if store string is included in string store', () => {
const $string = createStore('Hello world!');
const $findInString = createStore('Hello');
const $result = includes($string, $findInString);

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

it('should return true if store string is not included in string store', () => {
const $string = createStore('Hello world!');
const $findInString = createStore('Goodbye');
const $result = includes($string, $findInString);

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

it('should composed with other methods in patronum', () => {
const $string = createStore('Hello world!');
const $findInString = createStore('Goodbye');
const $result = not(includes($string, $findInString));

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

it('should throw an error if first argument is a number instead of array or string', () => {
const $number = createStore(5);
const $findNumber = createStore(5);

expect(() => includes(($number as any), $findNumber)).toThrowError(
'first argument should be an unit of array or string'
);
});
});
26 changes: 26 additions & 0 deletions src/includes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { combine, Store } from 'effector';

export function includes <T extends string>(
a: Store<T>,
b: Store<T> | T,
): Store<boolean>
export function includes <T extends string | number>(
a: Store<Array<T>>,
b: Store<T> | T,
): Store<boolean>
export function includes <T extends string | number>(
a: Store<T | Array<T>>,
b: Store<T> | T,
): Store<boolean> {
return combine(a, b, (a, b) => {
if (Array.isArray(a)) {
return a.includes(b);
}

if (typeof a === 'number') {
throw new Error('first argument should be an unit of array or string');
}

return a.includes(b as string);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a must be either an array or a string, so I would write it like this:

if (typeof a === 'string') {
      return a.includes(b as string);
}

throw new Error('First argument should be a unit of array or string');

I'm not a maintainer, just decided to leave a comment :)

});
}
88 changes: 88 additions & 0 deletions src/includes/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: includes
slug: includes
description: Checks if a value exists within a store containing a string or an array.
group: predicate
---

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

### Motivation

The includes method allows checking if a store (containing either a string or an array) includes a specified value. This value can either be written as a literal or provided through another store.

### Formulae

```ts
$isInclude = includes(container, value);
```

- `$isInclude` will be a store containing `true` if the `value` exists in `container`

### Arguments

1. `container: Store<string> | Store<Array<T>>` — A store with a string or an array where the `value` will be searched.
2. `value: T | Store<T>` — A literal value or a store containing the value to be checked for existence in `container`.

### Returns

- `$isInclude: Store<boolean>` — The store containing `true` if value exists within `container`, false otherwise.

### Example

#### Checking in Array

```ts
const $array = createStore([1, 2, 3]);
const $isInclude = includes($array, 2);

console.assert($isInclude.getState() === true);
```

#### Checking in String

```ts
const $string = createStore('Hello world!');
const $isInclude = includes($string, 'Hello');

console.assert($isInclude.getState() === true);
```

### Composition

The `includes` operator can be composed with other methods in patronum:

```ts
import { not } from 'patronum';

const $greeting = createStore('Hello world!');
const $isNotInclude = not(includes($greeting, 'Goodbye'));
// $isNotInclude contains `true` only when 'Goodbye' is not in $greeting
```

### Alternative

Compare to literal value:

```ts
import { createStore } from 'effector';
const $array = createStore([1, 2, 3]);
const $isInclude = $array.map((array) => array.includes(2));

console.assert($isInclude.getState() === true);
```

Compare to another store:

```ts
import { createStore, combine } from 'effector';
const $array = createStore([1, 2, 3]);
const $value = createStore(2);
const $isInclude = combine($array, $value, (array, value) => array.includes(value));

console.assert($isInclude.getState() === true);
```
58 changes: 58 additions & 0 deletions test-typings/includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expectType } from 'tsd';
import { createStore, Store } from 'effector';
import { includes } from '../dist/includes';

/**
* Should accept a string or array store with a compatible value
*/
{
expectType<Store<boolean>>(includes(createStore('Hello world!'), 'Hello'));
expectType<Store<boolean>>(includes(createStore(['a', 'b', 'c']), 'b'));
expectType<Store<boolean>>(includes(createStore([1, 2, 3]), 2));
}

/**
* Should accept a compatible value store
*/
{
const $stringStore = createStore('Hello world!');
const $searchString = createStore('Hello');
expectType<Store<boolean>>(includes($stringStore, $searchString));

const $arrayStore = createStore([1, 2, 3]);
const $searchNumber = createStore(2);
expectType<Store<boolean>>(includes($arrayStore, $searchNumber));
}

/**
* Should reject stores with incompatible types
*/
{
// @ts-expect-error
includes(createStore('Hello world!'), 1);
// @ts-expect-error
includes(createStore([1, 2, 3]), 'Hello');
// @ts-expect-error
includes(createStore(['a', 'b', 'c']), 1);
// @ts-expect-error
includes(createStore([1, 2, 3]), createStore('Hello'));
}

// Should reject stores and literals with incompatible types
{
// @ts-expect-error
includes(createStore(true), 'Hello');
// @ts-expect-error
includes('Hello', createStore(1));
// @ts-expect-error
includes(createStore([1, 2, 3]), true);
// @ts-expect-error
includes(createStore(['a', 'b', 'c']), 1);
}

// Should allow literal values compatible with the store type
{
expectType<Store<boolean>>(includes(createStore('Hello world!'), 'Hello'));
expectType<Store<boolean>>(includes(createStore(['a', 'b', 'c']), 'b'));
expectType<Store<boolean>>(includes(createStore([1, 2, 3]), 2));
}