From a07d4188af2ce081a9d39718aaac3a59fe412e41 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Thu, 25 May 2023 16:41:23 -0400 Subject: [PATCH] feat: Added more functions. --- .npmignore | 1 + docs/README.md | 6 ++ docs/modules.md | 172 ++++++++++++++++++++++++++++++++++++++++++++++ lib/index.d.ts | 7 +- lib/index.js | 38 +++++----- lib/ltrim.d.ts | 6 ++ lib/ltrim.js | 25 +++++++ lib/trim.d.ts | 6 ++ lib/trim.js | 25 +++++++ src/index.spec.ts | 53 +++++--------- src/index.ts | 31 +++------ src/trim.spec.ts | 83 ++++++++++++++++++++++ src/trim.ts | 28 ++++++++ 13 files changed, 401 insertions(+), 80 deletions(-) create mode 100644 lib/ltrim.d.ts create mode 100644 lib/ltrim.js create mode 100644 lib/trim.d.ts create mode 100644 lib/trim.js create mode 100644 src/trim.spec.ts create mode 100644 src/trim.ts diff --git a/.npmignore b/.npmignore index b98780e..0607f4c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ .envrc src +tsconfig.json flake.* \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 64cf9be..def7900 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,7 @@ @sbrow/strings / [Exports](modules.md) + +# Strings + +A small, simple, functional, and dependency-free library for JavaScript string manipulation. + +You can view the docs [here](./docs/modules.md) diff --git a/docs/modules.md b/docs/modules.md index a1746fc..5f0760b 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -6,12 +6,100 @@ ### Functions +- [afterFirst](modules.md#afterfirst) +- [afterLast](modules.md#afterlast) - [endsWith](modules.md#endswith) - [ltrim](modules.md#ltrim) +- [rtrim](modules.md#rtrim) - [startsWith](modules.md#startswith) +- [trim](modules.md#trim) ## Functions +### afterFirst + +▸ **afterFirst**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +Curry a [[Function]] + +**`Example`** + +```ts +import {F} from 'ts-toolbelt' + +/// If you are looking for creating types for `curry` +/// It handles placeholders and variable arguments +declare function curry(fn: Fn): F.Curry +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `P` | extends [separator: string \| typeof \_, str: string \| typeof \_] | +| `G` | extends readonly `any`[] = `GapsOf`<`P`, [separator: string, str: string]\> | +| `R` | extends `unknown` = `string` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `...p` | `P` \| [separator: string \| typeof \_, str: string \| typeof \_] | + +#### Returns + +`RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +[[Function]] + +#### Defined in + +node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 + +___ + +### afterLast + +▸ **afterLast**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +Curry a [[Function]] + +**`Example`** + +```ts +import {F} from 'ts-toolbelt' + +/// If you are looking for creating types for `curry` +/// It handles placeholders and variable arguments +declare function curry(fn: Fn): F.Curry +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `P` | extends [separator: string \| typeof \_, str: string \| typeof \_] | +| `G` | extends readonly `any`[] = `GapsOf`<`P`, [separator: string, str: string]\> | +| `R` | extends `unknown` = `string` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `...p` | `P` \| [separator: string \| typeof \_, str: string \| typeof \_] | + +#### Returns + +`RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +[[Function]] + +#### Defined in + +node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 + +___ + ### endsWith ▸ **endsWith**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> @@ -84,6 +172,48 @@ node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 ___ +### rtrim + +▸ **rtrim**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +Curry a [[Function]] + +**`Example`** + +```ts +import {F} from 'ts-toolbelt' + +/// If you are looking for creating types for `curry` +/// It handles placeholders and variable arguments +declare function curry(fn: Fn): F.Curry +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `P` | extends [cutset: string \| typeof \_, str: string \| typeof \_] | +| `G` | extends readonly `any`[] = `GapsOf`<`P`, [cutset: string, str: string]\> | +| `R` | extends `unknown` = `string` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `...p` | `P` \| [cutset: string \| typeof \_, str: string \| typeof \_] | + +#### Returns + +`RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +[[Function]] + +#### Defined in + +node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 + +___ + ### startsWith ▸ **startsWith**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> @@ -123,3 +253,45 @@ declare function curry(fn: Fn): F.Curry #### Defined in node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 + +___ + +### trim + +▸ **trim**<`P`, `G`, `R`\>(`...p`): `RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +Curry a [[Function]] + +**`Example`** + +```ts +import {F} from 'ts-toolbelt' + +/// If you are looking for creating types for `curry` +/// It handles placeholders and variable arguments +declare function curry(fn: Fn): F.Curry +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `P` | extends [cutset: string \| typeof \_, str: string \| typeof \_] | +| `G` | extends readonly `any`[] = `GapsOf`<`P`, [cutset: string, str: string]\> | +| `R` | extends `unknown` = `any` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `...p` | `P` \| [cutset: string \| typeof \_, str: string \| typeof \_] | + +#### Returns + +`RequiredKeys`<`ObjectOf`<`G`\>\> extends `never` ? `R` : `Curry`<(...`p`: `G`) => `R`\> + +[[Function]] + +#### Defined in + +node_modules/ts-toolbelt/out/Function/Curry.d.ts:77 diff --git a/lib/index.d.ts b/lib/index.d.ts index 7617f84..f39e444 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,6 +1,5 @@ +export * from './trim'; export declare const startsWith: import("ts-toolbelt/out/Function/Curry").Curry<(needle: string, haystack: string) => boolean>; export declare const endsWith: import("ts-toolbelt/out/Function/Curry").Curry<(needle: string, haystack: string) => boolean>; -/** - * Trims characters from the left side of a string. - */ -export declare const ltrim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => string>; +export declare const afterFirst: import("ts-toolbelt/out/Function/Curry").Curry<(separator: string, str: string) => string>; +export declare const afterLast: import("ts-toolbelt/out/Function/Curry").Curry<(separator: string, str: string) => string>; diff --git a/lib/index.js b/lib/index.js index cfee34a..345eca1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,31 +1,29 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ltrim = exports.endsWith = exports.startsWith = void 0; +exports.afterLast = exports.afterFirst = exports.endsWith = exports.startsWith = void 0; const utils_1 = require("./utils"); +__exportStar(require("./trim"), exports); function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } exports.startsWith = (0, utils_1.curry)((needle, haystack) => haystack.indexOf(needle) === 0); exports.endsWith = (0, utils_1.curry)((needle, haystack) => (new RegExp(`${escapeRegExp(needle)}$`)).test(haystack)); -/** - * Trims characters from the left side of a string. - */ -exports.ltrim = (0, utils_1.curry)((cutset, str) => { - const _trim = (x) => (0, exports.ltrim)(cutset)((0, utils_1.tail)(x)); - // const _trim = compose(ltrim(cutset), tail); - const trimmed = () => (0, utils_1.when)((x = '') => cutset.includes(x.charAt(0)), _trim)(str); - return (0, utils_1.isEmpty)(cutset) || (0, utils_1.isEmpty)(str) - ? str - : trimmed(); -}); -// export const afterFirst = curry( -// /** -// * @param {String} separator -// * @param {String} str -// * @return {String} -// */ -// (separator, str) => str.substring(str.indexOf(separator) + 1, str.length), -// ); +exports.afterFirst = (0, utils_1.curry)((separator, str) => str.substring(str.indexOf(separator) + 1, str.length)); +exports.afterLast = (0, utils_1.curry)((separator, str) => str.substring(str.lastIndexOf(separator) + 1, str.length)); // export const beforeFirst = curry( // /** // * diff --git a/lib/ltrim.d.ts b/lib/ltrim.d.ts new file mode 100644 index 0000000..714eae4 --- /dev/null +++ b/lib/ltrim.d.ts @@ -0,0 +1,6 @@ +/** + * Trims characters from the left side of a string. + */ +export declare const ltrim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => string>; +export declare const rtrim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => string>; +export declare const trim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => any>; diff --git a/lib/ltrim.js b/lib/ltrim.js new file mode 100644 index 0000000..7859bac --- /dev/null +++ b/lib/ltrim.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.trim = exports.rtrim = exports.ltrim = void 0; +const utils_1 = require("./utils"); +/** + * Trims characters from the left side of a string. + */ +exports.ltrim = (0, utils_1.curry)((cutset, str) => { + const _trim = (x) => (0, exports.ltrim)(cutset)((0, utils_1.tail)(x)); + // const _trim = compose(ltrim(cutset), tail); + const trimmed = () => (0, utils_1.when)((x = '') => cutset.includes(x.charAt(0)), _trim)(str); + return (0, utils_1.isEmpty)(cutset) || (0, utils_1.isEmpty)(str) + ? str + : trimmed(); +}); +exports.rtrim = (0, utils_1.curry)((cutset, str) => { + const _trim = (x) => (0, exports.rtrim)(cutset)(x.substring(0, x.length - 1)); + // const _trim = compose(rtrim(cutset), init); + const trimmed = () => (0, utils_1.when)((x = '') => cutset.includes(x.charAt(x.length - 1)), _trim)(str); + return (0, utils_1.isEmpty)(cutset) || (0, utils_1.isEmpty)(str) + ? str + : trimmed(); +}); +// const trim = curry((cutset: string, str: string) => compose(ltrim(cutset), rtrim(cutset))(str)); +exports.trim = (0, utils_1.curry)((cutset, str) => (0, exports.ltrim)(cutset)((0, exports.rtrim)(cutset, str))); diff --git a/lib/trim.d.ts b/lib/trim.d.ts new file mode 100644 index 0000000..714eae4 --- /dev/null +++ b/lib/trim.d.ts @@ -0,0 +1,6 @@ +/** + * Trims characters from the left side of a string. + */ +export declare const ltrim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => string>; +export declare const rtrim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => string>; +export declare const trim: import("ts-toolbelt/out/Function/Curry").Curry<(cutset: string, str: string) => any>; diff --git a/lib/trim.js b/lib/trim.js new file mode 100644 index 0000000..7859bac --- /dev/null +++ b/lib/trim.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.trim = exports.rtrim = exports.ltrim = void 0; +const utils_1 = require("./utils"); +/** + * Trims characters from the left side of a string. + */ +exports.ltrim = (0, utils_1.curry)((cutset, str) => { + const _trim = (x) => (0, exports.ltrim)(cutset)((0, utils_1.tail)(x)); + // const _trim = compose(ltrim(cutset), tail); + const trimmed = () => (0, utils_1.when)((x = '') => cutset.includes(x.charAt(0)), _trim)(str); + return (0, utils_1.isEmpty)(cutset) || (0, utils_1.isEmpty)(str) + ? str + : trimmed(); +}); +exports.rtrim = (0, utils_1.curry)((cutset, str) => { + const _trim = (x) => (0, exports.rtrim)(cutset)(x.substring(0, x.length - 1)); + // const _trim = compose(rtrim(cutset), init); + const trimmed = () => (0, utils_1.when)((x = '') => cutset.includes(x.charAt(x.length - 1)), _trim)(str); + return (0, utils_1.isEmpty)(cutset) || (0, utils_1.isEmpty)(str) + ? str + : trimmed(); +}); +// const trim = curry((cutset: string, str: string) => compose(ltrim(cutset), rtrim(cutset))(str)); +exports.trim = (0, utils_1.curry)((cutset, str) => (0, exports.ltrim)(cutset)((0, exports.rtrim)(cutset, str))); diff --git a/src/index.spec.ts b/src/index.spec.ts index ec41fdb..d53d8d3 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import fc from 'fast-check'; import { - /*afterFirstWord, beforeFirstWord, */ endsWith, ltrim, startsWith, + /*afterFirstWord, beforeFirstWord, */ afterFirst, afterLast, endsWith, ltrim, rtrim, trim, startsWith, } from './index'; describe('strings', () => { @@ -35,6 +35,24 @@ describe('strings', () => { })); }) }); + describe('afterFirst', () => { + it('removes everything before the first needle when possible', () => { + expect(afterFirst(' ', 'foo bar bat')).toBe('bar bat'); + }) + + it('removes nothing when needle is not present', () => { + expect(afterFirst('&', 'foo bar bat')).toBe('foo bar bat'); + }) + }) + describe('afterLast', () => { + it('removes everything after the last needle when possible', () => { + expect(afterLast(' ', 'foo bar bat')).toBe('bat'); + }) + + it('removes nothing when needle is not present', () => { + expect(afterLast('&', 'foo bar bat')).toBe('foo bar bat'); + }) + }) // describe('afterFirstWord', () => { // it('removes the first word', () => { // fc.assert(fc.property(fc.string(), (str) => { @@ -49,37 +67,4 @@ describe('strings', () => { // })); // }); // }); - describe('ltrim', () => { - it('returns input when cutset is empty', () => { - expect(ltrim('', 'foo')).toBe('foo'); - }); - it('returns empty when input is empty', () => { - expect(ltrim('foo', '')).toBe(''); - }); - it('can be curried', () => { - expect(ltrim('foo', '')).toBe(ltrim('foo')('')); - expect(ltrim(' ', 'foo')).toBe(ltrim(' ')('foo')); - }); - it('trims cutset', () => { - fc.assert(fc.property(fc.string(), fc.string(), (cutset, str) => { - const got = ltrim(cutset, str); - - if (cutset === '') { - expect(got).toBe(str); - } else if (str === '') { - expect(got).toBe(''); - } else { - if (got === '') { - for (const char of str) { - expect(cutset).contains(char); - } - } - - for (const char of cutset) { - expect(startsWith(char, got)).toBe(false); - } - } - })); - }); - }); }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index faa1ce9..fa620a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ -import { curry, isEmpty, tail, when } from "./utils"; +import { curry } from "./utils"; + +export * from './trim'; function escapeRegExp(str: string) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string @@ -8,28 +10,13 @@ export const startsWith = curry((needle: string, haystack: string) => haystack.i export const endsWith = curry((needle: string, haystack: string) => (new RegExp(`${escapeRegExp(needle)}$`)).test(haystack)); -/** - * Trims characters from the left side of a string. - */ -export const ltrim = curry((cutset: string, str: string) => { - const _trim = (x: string) => ltrim(cutset)(tail(x)); - // const _trim = compose(ltrim(cutset), tail); +export const afterFirst = curry( + (separator: string, str: string) => str.substring(str.indexOf(separator) + 1, str.length), +); - const trimmed: () => string = () => when((x: string = '') => cutset.includes(x.charAt(0)), _trim)(str); - - return isEmpty(cutset) || isEmpty(str) - ? str - : trimmed() -}); - -// export const afterFirst = curry( -// /** -// * @param {String} separator -// * @param {String} str -// * @return {String} -// */ -// (separator, str) => str.substring(str.indexOf(separator) + 1, str.length), -// ); +export const afterLast = curry( + (separator: string, str: string) => str.substring(str.lastIndexOf(separator) + 1, str.length), +); // export const beforeFirst = curry( // /** diff --git a/src/trim.spec.ts b/src/trim.spec.ts new file mode 100644 index 0000000..30ccdee --- /dev/null +++ b/src/trim.spec.ts @@ -0,0 +1,83 @@ +import fc from "fast-check"; +import { endsWith, startsWith } from "ramda"; +import { describe, expect, it } from "vitest"; + +import { ltrim, rtrim, trim } from "./trim"; + +describe('trim', () => { + describe('ltrim', () => { + it('returns input when cutset is empty', () => { + expect(ltrim('', 'foo')).toBe('foo'); + }); + it('returns empty when input is empty', () => { + expect(ltrim('foo', '')).toBe(''); + }); + it('can be curried', () => { + expect(ltrim('foo', '')).toBe(ltrim('foo')('')); + expect(ltrim(' ', ' foo')).toBe(ltrim(' ')(' foo')); + }); + it('trims cutset', () => { + fc.assert(fc.property(fc.string(), fc.string(), (cutset, str) => { + const got = ltrim(cutset, str); + + if (cutset === '') { + expect(got).toBe(str); + } else if (str === '') { + expect(got).toBe(''); + } else { + if (got === '') { + for (const char of str) { + expect(cutset).contains(char); + } + } + + for (const char of cutset) { + expect(startsWith(char, got)).toBe(false); + } + } + })); + }); + }); + + describe('rtrim', () => { + it('returns input when cutset is empty', () => { + expect(rtrim('', 'foo')).toBe('foo'); + }); + it('returns empty when input is empty', () => { + expect(rtrim('foo', '')).toBe(''); + }); + it('can be curried', () => { + expect(rtrim('foo', '')).toBe(rtrim('foo')('')); + expect(rtrim(' ', ' foo')).toBe(rtrim(' ')(' foo')); + }); + it('trims cutset', () => { + fc.assert(fc.property(fc.string(), fc.string(), (cutset, str) => { + const got = rtrim(cutset, str); + + if (cutset === '') { + expect(got).toBe(str); + } else if (str === '') { + expect(got).toBe(''); + } else { + if (got === '') { + for (const char of str) { + expect(cutset).contains(char); + } + } + + for (const char of cutset) { + expect(endsWith(char, got)).toBe(false); + } + } + })); + }); + }); + + describe('trim', () => { + it("composes 'ltrim' and 'rtrim'", function() { + fc.assert(fc.property(fc.string(), fc.string(), (cutset, str) => { + expect(trim(cutset, str)).toBe(ltrim(cutset, rtrim(cutset, str))); + })); + }); + }) +}); \ No newline at end of file diff --git a/src/trim.ts b/src/trim.ts new file mode 100644 index 0000000..672547b --- /dev/null +++ b/src/trim.ts @@ -0,0 +1,28 @@ +import { curry, isEmpty, tail, when } from "./utils"; + +/** + * Trims characters from the left side of a string. + */ + +export const ltrim = curry((cutset: string, str: string) => { + const _trim = (x: string) => ltrim(cutset)(tail(x)); + // const _trim = compose(ltrim(cutset), tail); + const trimmed: () => string = () => when((x: string = '') => cutset.includes(x.charAt(0)), _trim)(str); + + return isEmpty(cutset) || isEmpty(str) + ? str + : trimmed(); +}); + +export const rtrim = curry((cutset: string, str: string) => { + const _trim = (x: string) => rtrim(cutset)(x.substring(0, x.length - 1)); + // const _trim = compose(rtrim(cutset), init); + const trimmed: () => string = () => when((x: string = '') => cutset.includes(x.charAt(x.length - 1)), _trim)(str); + + return isEmpty(cutset) || isEmpty(str) + ? str + : trimmed(); +}); +// const trim = curry((cutset: string, str: string) => compose(ltrim(cutset), rtrim(cutset))(str)); + +export const trim = curry((cutset: string, str: string) => ltrim(cutset)(rtrim(cutset, str)));