diff --git a/.changeset/young-kiwis-watch.md b/.changeset/young-kiwis-watch.md new file mode 100644 index 00000000..f9a4c66c --- /dev/null +++ b/.changeset/young-kiwis-watch.md @@ -0,0 +1,18 @@ +--- +"@accelint/converters": minor +"@accelint/predicates": minor +--- + +The `toBoolean` function (packages/converters) centralizes the logic for coercing a value +to a boolean which enables the predicate functions (packages/predicates/src/is-noyes) to +be more specific in what they compare against rather than them simply being alias names +to broad validation. The available predicates are now: + +- `isAnyFalsy` +- `isAnyTruthy` +- `isFalse` +- `isTrue` +- `isOn` +- `isOff` +- `isNo` +- `isYes` diff --git a/packages/converters/package.json b/packages/converters/package.json index 4f07941f..7963c26c 100644 --- a/packages/converters/package.json +++ b/packages/converters/package.json @@ -39,7 +39,6 @@ }, "dependencies": { "@accelint/constants": "workspace:0.1.3", - "@accelint/predicates": "workspace:0.1.3", "typescript": "^5.6.3" }, "$schema": "https://json.schemastore.org/package", diff --git a/packages/converters/src/to-boolean/index.test.ts b/packages/converters/src/to-boolean/index.test.ts index 3b041ee7..a400e1f4 100644 --- a/packages/converters/src/to-boolean/index.test.ts +++ b/packages/converters/src/to-boolean/index.test.ts @@ -13,31 +13,57 @@ import { expect, it, describe } from 'vitest'; import { toBoolean } from './'; -const truthy = [1, '1', 'on', 'true', 'yes', true, 'ON', 'YES', 'TRUE']; -const falsey = [ +// biome-ignore lint/style/useNumberNamespace: testing value +const INFINITY = Infinity; + +const falsy = [ + '', 0, + 0.0, '0', - 'off', - 'false', - 'no', + '0.000', + '0000.000', false, + 'false', + ' FaLsE ', + void 0, + Number.NaN, + null, + undefined, +]; +const truthy = [ [], + 1, + '1', + true, + 'true', {}, - 'OFF', - 'NO', - 'FALSE', + 'any non-empty string', + // 'Yes', + // 'yes', + // 'No', + // 'no', + // 'off', + // 'Off', + // 'OFF', + // 'On', + // 'on', + INFINITY, + -INFINITY, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + /abc/, + new Date(), + new Error('Fun times.'), + () => void 0, ]; describe('toBoolean', () => { - for (const item of truthy) { - it(`should return true for ${item}`, () => { - expect(toBoolean(item)).toBeTruthy(); - }); - } + it.each(falsy)('%s', (val) => { + expect(toBoolean(val)).toBe(false); + }); - for (const item of falsey) { - it(`should return false for ${item}`, () => { - expect(toBoolean(item)).not.toBeTruthy(); - }); - } + it.each(truthy)('%s', (val) => { + expect(toBoolean(val)).toBe(true); + }); }); diff --git a/packages/converters/src/to-boolean/index.ts b/packages/converters/src/to-boolean/index.ts index 4c422126..e2542234 100644 --- a/packages/converters/src/to-boolean/index.ts +++ b/packages/converters/src/to-boolean/index.ts @@ -10,30 +10,29 @@ * governing permissions and limitations under the License. */ -import { isTrue } from '@accelint/predicates'; - /** - * Compare the given value against a custom list of `truthy` values. + * Returns true for any value not found to be a "false" value. * - * String values are not case sensitive. + * **"false" values** + * - inherently false values: '' (empty string), 0, false, undefined, null, NaN + * - numeric zero: '0.000' - any number of leading or trailing zeros + * - string literal: 'false' - any capitalizations or space-padding * - * _1, '1', 'on', 'true', 'yes', true_ + * For more restrictive comparisons against: true, false, on, off, yes, no; see + * the predicates package (\@accelint/predicates). * * @pure * * @example - * toBoolean('on'); - * // true - * - * toBoolean('yes'); - * // true - * - * toBoolean('off'); - * // false - * - * toBoolean('no'); - * // false + * toBoolean(1); // true + * toBoolean(' FaLsE '); // false + * toBoolean(' true'); // true + * toBoolean('000.000'); // false */ export function toBoolean(val: unknown) { - return isTrue(val); + return !( + !val || + `${val}`.trim().toLowerCase() === 'false' || + Number.parseFloat(`${val}`) === 0 + ); } diff --git a/packages/predicates/src/index.ts b/packages/predicates/src/index.ts index dbd01dc4..04743cd4 100644 --- a/packages/predicates/src/index.ts +++ b/packages/predicates/src/index.ts @@ -6,7 +6,16 @@ export { isBbox } from './is-bbox'; export { isLatitude } from './is-latitude'; export { isLongitude } from './is-longitude'; export { isNothing } from './is-nothing'; -export { isFalse, isNo, isOff, isOn, isTrue, isYes } from './is-noyes'; +export { + isAnyFalsy, + isAnyTruthy, + isFalse, + isNo, + isOff, + isOn, + isTrue, + isYes, +} from './is-noyes'; export { isFiniteNumber, isFiniteNumeric, diff --git a/packages/predicates/src/is-noyes/index.test.ts b/packages/predicates/src/is-noyes/index.test.ts index 4b584f10..0dc10060 100644 --- a/packages/predicates/src/is-noyes/index.test.ts +++ b/packages/predicates/src/is-noyes/index.test.ts @@ -10,26 +10,43 @@ * governing permissions and limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { isTrue, isYes, isFalse, isNo, isOn, isOff } from './'; +import { describe, expect, it } from 'vitest'; +import { + isAnyFalsy, + isAnyTruthy, + isFalse, + isNo, + isOff, + isOn, + isTrue, + isYes, +} from './'; -const truthy = [1, '1', 'on', 'true', 'yes', true, 'ON', 'YES', 'TRUE']; -const falsey = [0, '0', 'off', 'false', 'no', false, 'OFF', 'NO', 'FALSE']; +type Config = { + negative: unknown[]; + positive: unknown[]; + predicate: (a: unknown) => boolean; +}; -describe('boolean validators', () => { - for (const item of truthy) { - it(`should return true for ${item}`, () => { - expect(isOn(item)).toBeTruthy(); - expect(isTrue(item)).toBeTruthy(); - expect(isYes(item)).toBeTruthy(); - }); - } +const falsy = [0, '', false, ' false ', null, undefined, Number.NaN]; +const truthy = [1, true, ' true ']; - for (const item of falsey) { - it(`should return false for ${item}`, () => { - expect(isFalse(item)).toBeTruthy(); - expect(isOff(item)).toBeTruthy(); - expect(isNo(item)).toBeTruthy(); +describe('boolean predicates', () => { + describe.each` + predicate | positive | negative + ${isAnyFalsy} | ${[...falsy, 'no', 'off']} | ${[...truthy, 'on', 'yes']} + ${isAnyTruthy} | ${[...truthy, 'on', 'yes']} | ${[...falsy, 'no', 'off']} + ${isFalse} | ${[...falsy, ' false', '0']} | ${[...truthy, 'o', 'O', 'true', 'string with false', '0.00']} + ${isNo} | ${[...falsy, ' n', 'N ', 'no', 'NO']} | ${[...truthy, 'yes', 'string with no']} + ${isOff} | ${[...falsy, ' off', 'OFF ']} | ${[...truthy, 'on', 'string with off', 'of']} + ${isOn} | ${[...truthy, ' on', 'ON ']} | ${[...falsy, 'of', 'string with on', 'o']} + ${isTrue} | ${[...truthy, ' true']} | ${[...falsy, 'any string', {}, [], /abc/]} + ${isYes} | ${[...truthy, ' yes', 'YeS ', 'y']} | ${[...falsy, 'no', 'string with yes', 2]} + `('$predicate.name', ({ predicate, ...lists }: Config) => { + describe.each(['positive', 'negative'])('%s matches', (list) => { + it.each(lists[list] as unknown[])('%j', (val) => { + expect(predicate(val)).toBe(list === 'positive'); + }); }); - } + }); }); diff --git a/packages/predicates/src/is-noyes/index.ts b/packages/predicates/src/is-noyes/index.ts index ca059b68..39e2dbce 100644 --- a/packages/predicates/src/is-noyes/index.ts +++ b/packages/predicates/src/is-noyes/index.ts @@ -10,154 +10,190 @@ * governing permissions and limitations under the License. */ -// const trueRegex = /^(?:y|yes|true|1|on)$/i; -// const falseRegex = /^(?:n|no|false|0|off)$/i; +// NOTE: There is some conceptual overlap between these (predicates) functions, and the +// toBoolean (converters) function. The purposes of these two packages differ in intent: +// - the converter is only narrowly concerned with converting to a boolean +// - these functions will allow for a broader range of values to evaluate to boolean -// const test = (r: RegExp, val: unknown) => r.test(`${val}`.trim()); +const listFalse = ['', '0', 'false', 'nan', 'null', 'undefined']; +const listTrue = ['1', 'true']; -const falseValues = ['0', 'false', 'n', 'no', 'off']; -const trueValues = ['1', 'true', 'y', 'yes', 'on']; +const listNo = ['n', 'no', ...listFalse]; +const listOff = ['off', ...listFalse]; +const listOn = ['on', ...listTrue]; +const listYes = ['y', 'yes', ...listTrue]; const test = (list: string[], val: unknown) => list.includes(`${val}`.trim().toLowerCase()); /** - * Compare the given value against a custom list of `falsey` values. - * - * String values are not case sensitive. - * - * _0, '0', 'n', 'no', 'off', 'false', false_ + * Returns true if the given value is found in any of: + * - `isFalse(val)` + * - `isNo(val)` + * - `isOff(val)` * * @pure * * @example - * isFalse('on'); - * // false - * - * isFalse('yes'); - * // false + * isAnyFalsy(''); // true + * isAnyFalsy('no'); // true + * isAnyFalsy('off'); // true + * isAnyFalsy(0); // true + * isAnyFalsy(1); // false + * isAnyFalsy(true); // false + * isAnyFalsy('on'); // false + * isAnyFalsy('yes'); // false + */ +export const isAnyFalsy = (val: unknown) => + isFalse(val) || isNo(val) || isOff(val); + +/** + * Returns true if the given value is found in any of: + * - `isTrue(val)` + * - `isYes(val)` + * - `isOn(val)` * - * isFalse('off'); - * // true + * @pure * - * isFalse('no'); + * @example + * isAnyTruthy(''); // false + * isAnyTruthy('no'); // false + * isAnyTruthy('off'); // false + * isAnyTruthy(0); // false + * isAnyTruthy(1); // true + * isAnyTruthy(true); // true + * isAnyTruthy('on'); // true + * isAnyTruthy('yes'); // true */ -export const isFalse = (val: unknown) => test(falseValues, val); +export const isAnyTruthy = (val: unknown) => + isTrue(val) || isYes(val) || isOn(val); /** - * Compare the given value against a custom list of `falsey` values. + * Returns true if the given value is found in a case-insensitive list of + * "false" values. * - * String values are not case sensitive. + * False values: ['', '0', 'false', 'nan', 'null', 'undefined'] * - * _0, '0', 'n', 'no', 'off', 'false', false_ + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * * @pure * * @example - * isNo('on'); - * // false - * - * isNo('yes'); - * // false - * - * isNo('off'); - * // true - * - * isNo('no'); + * isFalse(''); // true + * isFalse(0); // true + * isFalse(1); // false + * isFalse(true); // false */ -export const isNo = isFalse; +export const isFalse = (val: unknown) => test(listFalse, val); /** - * Compare the given value against a custom list of `falsey` values. + * Returns true if the given value is found in a case-insensitive list of + * "no" values. + * + * False values: ['', '0', 'false', 'nan', 'null', 'undefined'] * - * String values are not case sensitive. + * Additional values: ['n', 'no'] * - * _0, '0', 'n', 'no', 'off', 'false', false_ + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * * @pure * * @example - * isOff('on'); - * // false - * - * isOff('yes'); - * // false - * - * isOff('off'); - * // true - * - * isOff('no'); + * isNo('n'); // true + * isNo(''); // true + * isNo(0); // true + * isNo(1); // false + * isNo(true); // false + * isNo('yes'); // false */ -export const isOff = isFalse; +export const isNo = (val: unknown) => test(listNo, val); /** - * Compare the given value against a custom list of `truthy` values. + * Returns true if the given value is found in a case-insensitive list of + * "off" values. * - * String values are not case sensitive. + * False values: ['', '0', 'false', 'nan', 'null', 'undefined'] * - * _1, '1', 'y', 'yes', 'on', 'true', true_ + * Additional values: ['off'] + * + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * * @pure * * @example - * isTrue('on'); - * // true - * - * isTrue('yes'); - * // true - * - * isTrue('off'); - * // false - * - * isTrue('no'); - * // false + * isOff('off'); // true + * isOff(''); // true + * isOff(0); // true + * isOff(1); // false + * isOff(true); // false + * isOff('on'); // false */ -export const isTrue = (val: unknown) => test(trueValues, val); +export const isOff = (val: unknown) => test(listOff, val); /** - * Compare the given value against a custom list of `truthy` values. + * Returns true if the given value is found in a case-insensitive list of + * "on" values. * - * String values are not case sensitive. + * True values: ['1', 'true'] * - * _1, '1', 'y', 'yes', 'on', 'true', true_ + * Additional values: ['on'] + * + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * * @pure * * @example - * isOn('on'); - * // true - * - * isOn('yes'); - * // true - * - * isOn('off'); - * // false - * - * isOn('no'); - * // false + * isOn('off'); // false + * isOn(''); // false + * isOn(0); // false + * isOn(1); // true + * isOn(true); // true + * isOn('on'); // true */ -export const isOn = isTrue; +export const isOn = (val: unknown) => test(listOn, val); /** - * Compare the given value against a custom list of `truthy` values. + * Returns true if the given value is found in a case-insensitive list of + * "true" values. * - * String values are not case sensitive. + * True values: ['1', 'true'] * - * _1, '1', 'y', 'yes', 'on', 'true', true_ + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * * @pure * * @example - * isYes('on'); - * // true + * isOn('no'); // false + * isOn(''); // false + * isOn(0); // false + * isOn(1); // true + * isOn(true); // true + * isOn('yes'); // true + */ +export const isTrue = (val: unknown) => test(listTrue, val); + +/** + * Returns true if the given value is found in a case-insensitive list of + * "yes" values. + * + * True values: ['1', 'true'] * - * isYes('yes'); - * // true + * Additional values: ['y', 'yes'] * - * isYes('off'); - * // false + * For a more liberal comparison/coercion to true or false see the converters + * package (\@accelint/converters). * - * isYes('no'); - * // false + * @pure + * + * @example + * isTrue(''); // false + * isTrue(0); // false + * isTrue(1); // true + * isTrue(true); // true */ -export const isYes = isTrue; +export const isYes = (val: unknown) => test(listYes, val); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d156edbc..825fe999 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,9 +166,6 @@ importers: '@accelint/constants': specifier: workspace:0.1.3 version: link:../constants - '@accelint/predicates': - specifier: workspace:0.1.3 - version: link:../predicates typescript: specifier: ^5.6.3 version: 5.6.3