diff --git a/src/babel-plugin-factories.json b/src/babel-plugin-factories.json index 7a63c8a4..5c3c1ff8 100644 --- a/src/babel-plugin-factories.json +++ b/src/babel-plugin-factories.json @@ -9,6 +9,7 @@ "patronum/either", "patronum/empty", "patronum/equals", + "patronum/includes", "patronum/every", "patronum/format", "patronum/in-flight", @@ -38,6 +39,7 @@ "either": "either", "empty": "empty", "equals": "equals", + "includes": "includes", "every": "every", "format": "format", "inFlight": "in-flight", diff --git a/src/includes/includes.fork.test.ts b/src/includes/includes.fork.test.ts new file mode 100644 index 00000000..a607306e --- /dev/null +++ b/src/includes/includes.fork.test.ts @@ -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(); + 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(); + 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' + ); +}); diff --git a/src/includes/includes.test.ts b/src/includes/includes.test.ts new file mode 100644 index 00000000..8f5fba4a --- /dev/null +++ b/src/includes/includes.test.ts @@ -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' + ); + }); +}); diff --git a/src/includes/index.ts b/src/includes/index.ts new file mode 100644 index 00000000..bf38574f --- /dev/null +++ b/src/includes/index.ts @@ -0,0 +1,26 @@ +import { combine, Store } from 'effector'; + +export function includes ( + a: Store, + b: Store | T, +): Store +export function includes ( + a: Store>, + b: Store | T, +): Store +export function includes ( + a: Store>, + b: Store | T, +): Store { + return combine(a, b, (a, b) => { + if (Array.isArray(a)) { + return a.includes(b); + } + + if (typeof a === 'string') { + return a.includes(b as string); + } + + throw new Error('first argument should be an unit of array or string'); + }); +} diff --git a/src/includes/readme.md b/src/includes/readme.md new file mode 100644 index 00000000..b0dc31ec --- /dev/null +++ b/src/includes/readme.md @@ -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 | Store>` — A store with a string or an array where the `value` will be searched. +2. `value: T | Store` — A literal value or a store containing the value to be checked for existence in `container`. + +### Returns + +- `$isInclude: Store` — 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); +``` diff --git a/test-typings/includes.ts b/test-typings/includes.ts new file mode 100644 index 00000000..59bd109b --- /dev/null +++ b/test-typings/includes.ts @@ -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>(includes(createStore('Hello world!'), 'Hello')); + expectType>(includes(createStore(['a', 'b', 'c']), 'b')); + expectType>(includes(createStore([1, 2, 3]), 2)); +} + +/** + * Should accept a compatible value store + */ +{ + const $stringStore = createStore('Hello world!'); + const $searchString = createStore('Hello'); + expectType>(includes($stringStore, $searchString)); + + const $arrayStore = createStore([1, 2, 3]); + const $searchNumber = createStore(2); + expectType>(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>(includes(createStore('Hello world!'), 'Hello')); + expectType>(includes(createStore(['a', 'b', 'c']), 'b')); + expectType>(includes(createStore([1, 2, 3]), 2)); +}