├── .github
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── build
├── set-version.sh
├── tsconfig.json
├── update-readme.sh
└── webpack.config.js
├── package-lock.json
├── package.json
├── src
├── Array.ts
├── Boolean.ts
├── Number.ts
├── Object.ts
├── String.ts
└── index.ts
└── tests
├── Array.test.ts
├── Boolean.test.ts
├── Number.test.ts
├── Object.test.ts
├── String.test.ts
└── jest.config.js
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 14
15 | - run: npm install
16 | - id: publish
17 | uses: JS-DevTools/npm-publish@v1
18 | with:
19 | token: ${{secrets.NPM_TOKEN}}
20 | - if: steps.publish.type != 'none'
21 | run: |
22 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}"
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 |
7 | jobs:
8 | test:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [14.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - run: npm ci
23 | - run: npm test
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | _bundles
3 | lib
4 | *.swp
5 | test.js
6 | test.ts
7 | .idea
8 | tmp
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Kyle Nazario
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TypeSwift
2 |
3 | Swift offers a number of convenience variables and functions across its core types. When I went back to TypeScript after
4 | a summer of Swift projects, I missed a lot of these conveniences. So I've added them to TypeScript and JavaScript.
5 |
6 | ## Installation
7 |
8 | `npm install typeswift`
9 |
10 | `yarn add typeswift`
11 |
12 | ## Usage
13 |
14 | For Node and single-page applications, import TypeSwift at the start of your index file.
15 |
16 | ```js
17 | // index.js
18 | const TypeSwift = require('typeswift');
19 |
20 | // rest of program...
21 | ```
22 |
23 | ```typescript
24 | // index.ts
25 | import 'typeswift';
26 |
27 | // rest of program...
28 | ```
29 |
30 | TypeSwift also comes bundled as minified and non-minified scripts. Add one of these in HTML to use them in subsequent
31 | `
38 |
43 | ```
44 |
45 | Once TypeSwift is loaded, you can begin using its extensions to JS's base classes:
46 |
47 | ```typescript
48 | const nums = ['one', 'two', 'three'];
49 | console.log(nums.first); // 'one'
50 | console.log(nums[0]); // 'one'
51 | console.log(nums.last) // 'three'
52 | console.log(nums.randomElement()) // 'two'
53 |
54 | const objectsMayHaveId = [{id: 1}, {}, {id: 3}];
55 | const ids = objectsMayHaveId.compactMap(object => object.id);
56 | console.log(ids); // [1, 3]
57 |
58 | const objectA = {one: 1, two: 2};
59 | const objectB = {two: 'two', three: 'three'};
60 | const merged = objectA.merging(objectB, (objA, objB) => objB);
61 | console.log(merged); // {one: 1, two: 'two', three: 'three'}
62 |
63 | const letters = 'abc';
64 | console.log(letters.reversed()) // 'cba'
65 | console.log(letters.shuffled()) // 'cab'
66 | console.log(letters.endIndex) // 2
67 | ```
68 |
69 | ## Features
70 |
71 | TypeSwift includes a number of convenience methods based on their Swift counterparts. It does not include:
72 |
73 | - Functionality around managing memory
74 | - Functions that mutate primitive types, since [JS primitives are immutable][1] (e.x. you can't `.toggle()` booleans)
75 | - Functions with an extremely close equivalent in JavaScript (e.x. `first(where: )` is just `find()`)
76 |
77 | [1]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive
78 |
79 | ### API
80 |
81 | ```typescript
82 | type UniquingKeysCallback = (valueOne: any, valueTwo: any) => any;
83 | type KeyedObject = {[key: string]: any};
84 | type KeyValueAssertionCallback = (key: string | number, value: E) => boolean;
85 |
86 | declare global {
87 | interface Object {
88 | readonly isEmpty: boolean;
89 | readonly count: number;
90 | readonly first: any;
91 | readonly randomElement: () => any;
92 | readonly merge: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => void;
93 | readonly merging: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => KeyedObject;
94 | readonly removeValue: (forKey: string | number) => void;
95 | readonly removeAll: () => void;
96 | readonly contains: (where: KeyValueAssertionCallback) => boolean;
97 | readonly allSatisfy: (callback: KeyValueAssertionCallback) => boolean;
98 | }
99 | }
100 | ```
101 |
102 | ```typescript
103 | declare global {
104 | interface Array {
105 | readonly first: T | undefined;
106 | readonly last: T | undefined;
107 | readonly isEmpty: boolean;
108 | readonly randomElement: () => T | undefined;
109 | readonly insert: (element: T, at: number) => void;
110 | readonly remove: (at: number) => T | undefined;
111 | readonly compactMap: (callback: (element: T, index?: number, parent?: Array) => any | undefined) => Array;
112 | readonly shuffle: () => void;
113 | readonly shuffled: () => Array;
114 | readonly swapAt: (indexA: number, indexB: number) => void;
115 | readonly startIndex: number | undefined;
116 | readonly endIndex: number | undefined;
117 | }
118 | }
119 | ```
120 |
121 | ```typescript
122 | declare global {
123 | interface String {
124 | readonly isEmpty: boolean;
125 | readonly inserted: (substring: string, at: number) => string;
126 | readonly first: string | undefined;
127 | readonly last: string | undefined;
128 | readonly randomElement: () => string | undefined;
129 | readonly map: (callback: (char: string, index?: number, parent?: string) => string) => string;
130 | readonly compactMap: (callback: (char: string, index?: number, parent?: string) => string | undefined) => string;
131 | readonly forEach: (callback: (char: string, index?: number, parent?: string) => void) => void;
132 | readonly reduce: (callback: (result: R, char: string, index?: number) => R, initialValue: R) => R;
133 | readonly sorted: () => string;
134 | readonly reversed: () => string;
135 | readonly shuffled: () => string;
136 | readonly startIndex: number | undefined;
137 | readonly endIndex: number | undefined;
138 | readonly prefix: (callback: (char: string, index?: number, parent?: string) => boolean) => string;
139 | }
140 | }
141 | ```
142 |
143 | ```typescript
144 | declare global {
145 | interface Number {
146 | readonly quotientAndRemainderOf: (dividingBy: number) => [number, number];
147 | readonly isMultipleOf: (number: number) => boolean;
148 | readonly isZero: boolean;
149 | }
150 |
151 | // static properties (e.x. Number.zero)
152 | interface NumberConstructor {
153 | readonly zero: 0;
154 | }
155 | }
156 | ```
157 |
158 | ```typescript
159 | declare global {
160 | // static properties (e.x. Boolean.random())
161 | interface BooleanConstructor {
162 | readonly random: () => boolean;
163 | }
164 | }
165 | ```
166 |
167 | ## Q&A
168 |
169 | **Can I still use `arr[0]`? Do I have to use `arr.first`?**
170 |
171 | TypeSwift doesn't break any existing JS functionality, it just extends it. All existing JS methods and variables will
172 | still work.
173 |
174 | **Why should I install a whole package just to write `arr.last`?**
175 |
176 | If you don't think TypeSwift's extensions would help you, that's cool. I prefer writing
177 | `arr.last` instead of `arr[arr.length - 1]`, but it's all personal preference.
178 |
179 | **I would like X feature from Swift added.**
180 |
181 | Help with features would be greatly appreciated. Please check out existing code to see the style and [open a PR][2].
182 |
183 | [2]: https://github.com/kyle-n/TypeSwift/pulls
184 |
--------------------------------------------------------------------------------
/build/set-version.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPTPATH="$(cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P)"
4 | cd "$SCRIPTPATH"
5 | cd ..
6 | ./build/update-readme.sh
7 | git add README.md
8 | git commit -m "Updates README"
9 | npm version $1
10 |
--------------------------------------------------------------------------------
/build/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "lib": ["ES2020", "dom"],
6 | "declaration": true,
7 | "declarationMap": true,
8 | "sourceMap": true,
9 | "outDir": "../lib",
10 | "rootDir": "../src",
11 | "strict": true,
12 | "noUnusedLocals": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitReturns": false,
15 | "allowSyntheticDefaultImports": true,
16 | "esModuleInterop": true,
17 | "forceConsistentCasingInFileNames": true
18 | },
19 | "include": [
20 | "../src"
21 | ],
22 | "exclude": [
23 | "../tests"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/build/update-readme.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | newline=$'\n'
3 |
4 | # Run self in project root, no matter where script is triggered from
5 | SCRIPTPATH="$(cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P)"
6 | cd "$SCRIPTPATH"
7 | cd ..
8 |
9 | # Remove previous content
10 | gsed -i '/^###\sAPI/,/^#/{/^#/!d}' README.md
11 |
12 | # Grab line number of api section
13 | api_line_number=$(grep -n '### API' README.md | cut -d: -f 1)
14 | api_line_number=$((api_line_number+1))
15 |
16 | code_header='```typescript'
17 | code_footer='```'
18 | gsed -i "${api_line_number}i ${newline}" README.md
19 |
20 | for class in 'Boolean' 'Number' 'String' 'Array' 'Object'
21 | do
22 | # Blank lines around section
23 | gsed -i "${api_line_number}i ${newline}" README.md
24 | next_line=$((api_line_number+1))
25 |
26 | # file output
27 | file="src/${class}.ts"
28 | content=$(gsed -n '/^export {}/,/^Object\.defineProperties/p;/^Object\.defineProperties/q' $file | gtail -n +2 | ghead -n -2)
29 | formatted_content=$(echo -e "${content}\n" | gsed ':a $!{N; ba}; s/\n/\\n/g')
30 |
31 | # Wrap header declaration and insert into README
32 | insertion=$(gsed -i "${next_line}i ${code_header}${formatted_content}${code_footer}" README.md)
33 | done
34 |
35 | echo "README updated"
36 |
--------------------------------------------------------------------------------
/build/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const sharedConfig = {
4 | resolve: {
5 | extensions: ['.js'],
6 | },
7 | output: {
8 | filename: '[name].js',
9 | path: path.resolve(__dirname, '..', '_bundles'),
10 | },
11 | };
12 |
13 | const devConfig = {
14 | ...sharedConfig,
15 | entry: {
16 | 'typeswift': './lib/index.js'
17 | },
18 | devtool: 'source-map',
19 | mode: 'development'
20 | };
21 |
22 | const prodConfig = {
23 | ...sharedConfig,
24 | entry: {
25 | 'typeswift.min': './lib/index.js'
26 | },
27 | mode: 'production'
28 | };
29 |
30 | module.exports = [devConfig, prodConfig]
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeswift",
3 | "version": "1.1.7",
4 | "description": "Swift-like extensions for JavaScript and TypeScript",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "type": "commonjs",
8 | "scripts": {
9 | "compile": "tsc -p build/tsconfig.json && webpack -c build/webpack.config.js",
10 | "clean": "shx rm -rf _bundles lib",
11 | "build": "npm run clean && npm run compile",
12 | "prepare": "npm run build",
13 | "preview-npm-files": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz",
14 | "test": "jest -c tests/jest.config.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/kyle-n/TypeSwift.git"
19 | },
20 | "keywords": [
21 | "swift",
22 | "typescript",
23 | "javascript"
24 | ],
25 | "author": "Kyle Nazario",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/kyle-n/TypeSwift/issues"
29 | },
30 | "homepage": "https://github.com/kyle-n/TypeSwift#readme",
31 | "devDependencies": {
32 | "@types/jest": "^26.0.15",
33 | "@types/node": "^14.14.6",
34 | "jest": "^26.6.3",
35 | "shx": "^0.3.3",
36 | "ts-jest": "^26.4.4",
37 | "typescript": "^4.5.2",
38 | "webpack": "^5.64.2",
39 | "webpack-cli": "^4.9.1"
40 | },
41 | "files": [
42 | "lib/*",
43 | "_bundles/*",
44 | "package.json"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/src/Array.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface Array {
5 | readonly first: T | undefined;
6 | readonly last: T | undefined;
7 | readonly isEmpty: boolean;
8 | readonly randomElement: () => T | undefined;
9 | readonly insert: (element: T, at: number) => void;
10 | readonly remove: (at: number) => T | undefined;
11 | readonly compactMap: (callback: (element: T, index?: number, parent?: Array) => any | undefined) => Array;
12 | readonly shuffle: () => void;
13 | readonly shuffled: () => Array;
14 | readonly swapAt: (indexA: number, indexB: number) => void;
15 | readonly startIndex: number | undefined;
16 | readonly endIndex: number | undefined;
17 | }
18 | }
19 |
20 | Object.defineProperties(Array.prototype, {
21 | first: {
22 | get(this: Array) {
23 | if (this.length > 0) return this[0];
24 | else return undefined;
25 | }
26 | },
27 | last: {
28 | get(this: Array) {
29 | if (this.length > 0) return this[this.length - 1];
30 | else return undefined;
31 | }
32 | },
33 | isEmpty: {
34 | get(this: Array) {
35 | return this.length < 1;
36 | }
37 | },
38 | randomElement: {
39 | get(this: Array) {
40 | return () => {
41 | if (this.isEmpty) return undefined;
42 | const index = Math.floor(Math.random() * this.length);
43 | return this[index];
44 | };
45 | }
46 | },
47 | insert: {
48 | get(this: Array) {
49 | return (element: any, at: number) => {
50 | this.splice(at, 0, element);
51 | }
52 | }
53 | },
54 | remove: {
55 | get(this: Array) {
56 | return (at: number) => {
57 | this.splice(at, 1);
58 | }
59 | }
60 | },
61 | compactMap: {
62 | get(this: Array) {
63 | return (callback: (element: T, index?: number, parent?: Array) => any | undefined): Array => {
64 | const results: Array = [];
65 | for (let i = 0; i < this.length; i++) {
66 | const result = callback(this[i], i, this);
67 | if (result !== undefined && result !== null) results.push(result);
68 | }
69 | return results;
70 | };
71 | }
72 | },
73 | shuffled: {
74 | get(this: Array) {
75 | return () => {
76 | const clone = this.slice();
77 | clone.shuffle();
78 | return clone;
79 | };
80 | }
81 | },
82 | shuffle: {
83 | get(this: Array) {
84 | return () => {
85 | for (let i = this.endIndex ?? -1; i > 0; i--) {
86 | const j = Math.floor(Math.random() * (i + 1));
87 | let tmp = this[i];
88 | this[i] = this[j];
89 | this[j] = tmp;
90 | }
91 | };
92 | }
93 | },
94 | swapAt: {
95 | get(this: Array) {
96 | return (indexA: number, indexB: number) => {
97 | const temp = this[indexB];
98 | this[indexB] = this[indexA];
99 | this[indexA] = temp;
100 | };
101 | }
102 | },
103 | startIndex: {
104 | get(this: Array) {
105 | const indices = Object.keys(this);
106 | if (indices.isEmpty || typeof indices.first !== 'string') return undefined;
107 | const startIndex: number = parseInt(indices.first);
108 | if (isNaN(startIndex)) return undefined;
109 | else return startIndex;
110 | }
111 | },
112 | endIndex: {
113 | get(this: Array) {
114 | const indices = Object.keys(this);
115 | if (indices.isEmpty || typeof indices.last !== 'string') return undefined;
116 | const endIndex: number = parseInt(indices.last);
117 | if (isNaN(endIndex)) return undefined;
118 | else return endIndex;
119 | }
120 | }
121 | });
122 |
--------------------------------------------------------------------------------
/src/Boolean.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | // static properties (e.x. Boolean.random())
5 | interface BooleanConstructor {
6 | readonly random: () => boolean;
7 | }
8 | }
9 |
10 | Object.defineProperties(Boolean, {
11 | random: {
12 | get(this: Boolean) {
13 | return () => Math.random() > 0.5;
14 | }
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/Number.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface Number {
5 | readonly quotientAndRemainderOf: (dividingBy: number) => [number, number];
6 | readonly isMultipleOf: (number: number) => boolean;
7 | readonly isZero: boolean;
8 | }
9 |
10 | // static properties (e.x. Number.zero)
11 | interface NumberConstructor {
12 | readonly zero: 0;
13 | }
14 | }
15 |
16 | Object.defineProperties(Number, {
17 | zero: {
18 | get(this: Number) {
19 | return 0;
20 | }
21 | }
22 | });
23 |
24 | Object.defineProperties(Number.prototype, {
25 | quotientAndRemainderOf: {
26 | get(this: Number) {
27 | return (dividingBy: number) => {
28 | const n: number = this.valueOf();
29 | const divisionResult = n / dividingBy;
30 | const quotient = Math.floor(divisionResult);
31 | const remainder = n - (quotient * dividingBy);
32 | return [quotient, remainder];
33 | };
34 | }
35 | },
36 | isMultipleOf: {
37 | get(this: Number) {
38 | return (of: number) => {
39 | const n: number = this.valueOf();
40 | return (n % of === 0);
41 | };
42 | }
43 | },
44 | isZero: {
45 | get(this: Number) {
46 | return this.valueOf() === 0;
47 | }
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/src/Object.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | type UniquingKeysCallback = (valueOne: any, valueTwo: any) => any;
4 | type KeyedObject = {[key: string]: any};
5 | type KeyValueAssertionCallback = (key: string | number, value: E) => boolean;
6 |
7 | declare global {
8 | interface Object {
9 | readonly isEmpty: boolean;
10 | readonly count: number;
11 | readonly first: any;
12 | readonly randomElement: () => any;
13 | readonly merge: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => void;
14 | readonly merging: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => KeyedObject;
15 | readonly removeValue: (forKey: string | number) => void;
16 | readonly removeAll: () => void;
17 | readonly contains: (where: KeyValueAssertionCallback) => boolean;
18 | readonly allSatisfy: (callback: KeyValueAssertionCallback) => boolean;
19 | }
20 | }
21 |
22 | Object.defineProperties(Object.prototype, {
23 | isEmpty: {
24 | get(this: Object) {
25 | return Object.keys(this).length === 0;
26 | }
27 | },
28 | count: {
29 | get(this: Object) {
30 | return Object.keys(this).length;
31 | }
32 | },
33 | first: {
34 | get(this: Object) {
35 | return Object.values(this)[0];
36 | }
37 | },
38 | randomElement: {
39 | get(this: Object) {
40 | return () => {
41 | const index = Math.floor(Math.random() * Object.values(this).length);
42 | return Object.values(this)[index];
43 | };
44 | }
45 | },
46 | merge: {
47 | get(this: KeyedObject) {
48 | return (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback): void => {
49 | Object.keys(objectToMerge).forEach(key => {
50 | if (this.hasOwnProperty(key)) this[key] = uniquingKeysWith(this[key], objectToMerge[key]);
51 | else this[key] = objectToMerge[key];
52 | });
53 | };
54 | }
55 | },
56 | merging: {
57 | get(this: Object) {
58 | return (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback): KeyedObject => {
59 | const merged = {...this};
60 | merged.merge(objectToMerge, uniquingKeysWith);
61 | return merged;
62 | };
63 | }
64 | },
65 | removeValue: {
66 | get(this: any) {
67 | return (forKey: string | number): void => {
68 | delete this[forKey];
69 | };
70 | }
71 | },
72 | removeAll: {
73 | get(this: KeyedObject) {
74 | return (): void => {
75 | Object.keys(this).forEach(key => delete this[key]);
76 | };
77 | }
78 | },
79 | contains: {
80 | get(this: KeyedObject) {
81 | return (where: KeyValueAssertionCallback): boolean => {
82 | for (const key in this) {
83 | if (where(key, this[key])) return true;
84 | }
85 | return false;
86 | };
87 | }
88 | },
89 | allSatisfy: {
90 | get(this: KeyedObject) {
91 | return (callback: KeyValueAssertionCallback): boolean => {
92 | for (const key in this) {
93 | if (!callback(key, this[key])) return false;
94 | }
95 | return true;
96 | };
97 | }
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/src/String.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface String {
5 | readonly isEmpty: boolean;
6 | readonly inserted: (substring: string, at: number) => string;
7 | readonly first: string | undefined;
8 | readonly last: string | undefined;
9 | readonly randomElement: () => string | undefined;
10 | readonly map: (callback: (char: string, index?: number, parent?: string) => string) => string;
11 | readonly compactMap: (callback: (char: string, index?: number, parent?: string) => string | undefined) => string;
12 | readonly forEach: (callback: (char: string, index?: number, parent?: string) => void) => void;
13 | readonly reduce: (callback: (result: R, char: string, index?: number) => R, initialValue: R) => R;
14 | readonly sorted: () => string;
15 | readonly reversed: () => string;
16 | readonly shuffled: () => string;
17 | readonly startIndex: number | undefined;
18 | readonly endIndex: number | undefined;
19 | readonly prefix: (callback: (char: string, index?: number, parent?: string) => boolean) => string;
20 | }
21 | }
22 |
23 | Object.defineProperties(String.prototype, {
24 | isEmpty: {
25 | get(this: String) {
26 | return this.valueOf().length === 0;
27 | }
28 | },
29 | inserted: {
30 | get(this: String) {
31 | return (substring: string, at: number) => {
32 | return this.slice(0, at) + substring + this.slice(at);
33 | };
34 | }
35 | },
36 | first: {
37 | get(this: String) {
38 | if (this.length > 0) return this[0];
39 | else return undefined;
40 | }
41 | },
42 | last: {
43 | get(this: String) {
44 | if (this.length > 0) return this[this.length - 1];
45 | else return undefined;
46 | }
47 | },
48 | randomElement: {
49 | get(this: String) {
50 | return () => {
51 | if (this.length < 1) return undefined;
52 | const index = Math.floor(Math.random() * this.length);
53 | return this[index];
54 | };
55 | }
56 | },
57 | map: {
58 | get(this: String) {
59 | return (callback: (char: string, index?: number, parent?: string) => string) => {
60 | let mappedString = '';
61 | for (let i = 0; i < this.length; i++) {
62 | mappedString += callback(this[i], i, this.valueOf());
63 | }
64 | return mappedString;
65 | };
66 | }
67 | },
68 | compactMap: {
69 | get(this: String) {
70 | return (callback: (char: string, index?: number, parent?: string) => string | undefined) => {
71 | let mappedString = '';
72 | for (let i = 0; i < this.length; i++) {
73 | const mappedChar = callback(this[i], i, this.valueOf());
74 | if (typeof mappedChar === 'string') mappedString += mappedChar;
75 | }
76 | return mappedString;
77 | };
78 | }
79 | },
80 | forEach: {
81 | get(this: String) {
82 | return (callback: (char: string, index?: number, parent?: string) => void) => {
83 | for (let i = 0; i < this.length; i++) {
84 | callback(this[i], i, this.valueOf());
85 | }
86 | };
87 | }
88 | },
89 | reduce: {
90 | get(this: String) {
91 | return (callback: (result: R, char: string, index?: number) => R, initialValue: R): R => {
92 | let val = initialValue;
93 | for (let i = 0; i < this.length; i++) {
94 | val = callback(val, this[i], i);
95 | }
96 | return val;
97 | };
98 | }
99 | },
100 | startIndex: {
101 | get(this: String) {
102 | if (this.length > 0) return 0;
103 | else return undefined;
104 | }
105 | },
106 | endIndex: {
107 | get(this: String) {
108 | if (this.length > 0) return this.length - 1;
109 | else return undefined;
110 | }
111 | },
112 | sorted: {
113 | get(this: String) {
114 | return () => {
115 | return this.split('').sort().join('');
116 | };
117 | }
118 | },
119 | reversed: {
120 | get(this: String) {
121 | return () => {
122 | let reversed = '';
123 | for (let i = this.endIndex ?? -1; i >= 0; i--) {
124 | reversed += this[i];
125 | }
126 | return reversed;
127 | }
128 | }
129 | },
130 | shuffled: {
131 | get(this: String) {
132 | return () => {
133 | return this.split('').shuffled().join('')
134 | }
135 | }
136 | },
137 | prefix: {
138 | get(this: String) {
139 | return (callback: (char: string, index?: number, parent?: string) => boolean): string => {
140 | let prefix = '';
141 | for (let i = 0; i < this.length; i++) {
142 | const charPassesCallback = callback(this[i], i, this.valueOf());
143 | if (charPassesCallback) prefix += this[i];
144 | else break;
145 | }
146 | return prefix;
147 | };
148 | }
149 | }
150 | });
151 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Array';
2 | export * from './Number';
3 | export * from './String';
4 | export * from './Object';
5 | export * from './Boolean';
6 |
--------------------------------------------------------------------------------
/tests/Array.test.ts:
--------------------------------------------------------------------------------
1 | import '../src';
2 |
3 | const getExample = () => [1, 2, undefined, 4];
4 |
5 | test('Array.first', () => {
6 | expect(getExample().first).toBe(1);
7 | expect([].first).toBe(undefined);
8 | });
9 |
10 | test('Array.last', () => {
11 | expect(getExample().last).toBe(4);
12 | expect([].last).toBe(undefined);
13 | });
14 |
15 | test('Array.isEmpty', () => {
16 | expect(getExample().isEmpty).toBe(false);
17 | expect([].isEmpty).toBe(true);
18 | });
19 |
20 | test('Array.randomElement()', () => {
21 | const num = [1];
22 | expect(num.randomElement()).toBe(1);
23 | expect([].randomElement()).toBe(undefined);
24 | });
25 |
26 | test('Array.insert()', () => {
27 | const example = getExample();
28 | example.insert(99, 0);
29 | expect(example).toEqual([99, 1, 2, undefined, 4]);
30 | });
31 |
32 | test('Array.remove()', () => {
33 | const example = getExample();
34 | example.remove(0);
35 | expect(example).toEqual([2, undefined, 4]);
36 | });
37 |
38 | test('Array.compactMap()', () => {
39 | const nonNullOrUndefinedElements = getExample().compactMap(elem => elem && elem * 2);
40 | expect(nonNullOrUndefinedElements).toEqual([2, 4, 8]);
41 | });
42 |
43 | test('Array.shuffled()', () => {
44 | const example = getExample();
45 | const cloned = example.shuffled();
46 | expect(example).not.toBe(cloned);
47 | });
48 |
49 | test('Array.swapAt()', () => {
50 | const example = getExample();
51 | example.swapAt(0, 1);
52 | expect(example).toEqual([2, 1, undefined, 4]);
53 | });
54 |
55 | test('Array.startIndex', () => {
56 | expect(getExample().startIndex).toBe(0);
57 | expect([].startIndex).toBe(undefined);
58 | });
59 |
60 | test('Array.endIndex', () => {
61 | expect(getExample().endIndex).toBe(3);
62 | expect([].endIndex).toBe(undefined);
63 | });
64 |
--------------------------------------------------------------------------------
/tests/Boolean.test.ts:
--------------------------------------------------------------------------------
1 | import '../src';
2 |
3 | test('Boolean.random()', () => {
4 | expect(typeof Boolean.random()).toBe('boolean');
5 | });
6 |
--------------------------------------------------------------------------------
/tests/Number.test.ts:
--------------------------------------------------------------------------------
1 | import '../src';
2 |
3 | test('Number.prototype.quotientAndRemainder()', () => {
4 | const n = 42;
5 | expect(n.quotientAndRemainderOf(10)).toEqual([4, 2]);
6 | });
7 |
8 | test('Number.prototype.isMultipleOf()', () => {
9 | const n = 10;
10 | expect(n.isMultipleOf(2)).toBe(true);
11 | expect(n.isMultipleOf(3)).toBe(false);
12 | });
13 |
14 | test('Number.prototype.isZero', () => {
15 | const one = 1;
16 | const zero = 0;
17 | expect(one.isZero).toBe(false);
18 | expect(zero.isZero).toBe(true);
19 | });
20 |
21 | test('Number.zero', () => {
22 | expect(Number.zero).toBe(0);
23 | });
24 |
--------------------------------------------------------------------------------
/tests/Object.test.ts:
--------------------------------------------------------------------------------
1 | import '../src';
2 |
3 | const getEmpty = () => ({});
4 | const getObject = () => ({one: 1, two: 2});
5 |
6 | test('Object.isEmpty', () => {
7 | expect(getEmpty().isEmpty).toBe(true);
8 | expect(getObject().isEmpty).toBe(false);
9 | });
10 |
11 | test('Object.count', () => {
12 | expect(getEmpty().count).toBe(0);
13 | expect(getObject().count).toBe(2);
14 | });
15 |
16 | test('Object.first', () => {
17 | expect(getEmpty().first).toBe(undefined);
18 | expect(getObject().first).toBe(1);
19 | });
20 |
21 | test('Object.randomElement()', () => {
22 | expect(getEmpty().randomElement()).toBe(undefined);
23 | expect(typeof getObject().randomElement()).toBe('number');
24 | });
25 |
26 | test('Object.merge()', () => {
27 | const obj = getObject();
28 | obj.merge({two: 'two', three: 'three'}, (objA, objB) => objB);
29 | expect(obj).toEqual({one: 1, two: 'two', three: 'three'});
30 | });
31 |
32 | test('Object.merged()', () => {
33 | const obj = getObject();
34 | const merged = obj.merging({two: 'two', three: 'three'}, (objA, objB) => objB);
35 | expect(merged).toEqual({one: 1, two: 'two', three: 'three'});
36 | expect(obj).not.toBe(merged);
37 | });
38 |
39 | test('Object.removeValue', () => {
40 | const obj = getObject();
41 | obj.removeValue('one');
42 | expect(obj).toEqual({two: 2});
43 | });
44 |
45 | test('Object.removeAll()', () => {
46 | const obj = getObject();
47 | obj.removeAll();
48 | expect(obj).toEqual({});
49 | });
50 |
51 | test('Object.contains()', () => {
52 | const obj: any = getObject();
53 | const hasOne = obj.contains((key: string, val: number) => {
54 | return key === 'one' && val === 1;
55 | });
56 | const hasFive = obj.contains((key: string, val: number) => {
57 | return key === 'five' && val === 5;
58 | });
59 | expect(hasOne).toBe(true);
60 | expect(hasFive).toBe(false);
61 | });
62 |
63 | test('Object.allSatisfy()', () => {
64 | const obj = getObject();
65 | const allNumbers = obj.allSatisfy((key, val) => {
66 | return typeof val === 'number';
67 | });
68 | const allBooleans = obj.allSatisfy((key, val) => {
69 | return typeof val === 'boolean';
70 | });
71 | expect(allNumbers).toBe(true);
72 | expect(allBooleans).toBe(false);
73 | });
74 |
--------------------------------------------------------------------------------
/tests/String.test.ts:
--------------------------------------------------------------------------------
1 | import '../src';
2 |
3 | const getEmpty = () => '';
4 | const getABC = () => 'abc';
5 |
6 | test('String.isEmpty', () => {
7 | expect(getEmpty().isEmpty).toBe(true);
8 | expect(getABC().isEmpty).toBe(false);
9 | });
10 |
11 | test('String.inserted()', () => {
12 | expect(getABC().inserted('x', 0)).toBe('xabc');
13 | });
14 |
15 | test('String.first', () => {
16 | expect(getEmpty().first).toBe(undefined);
17 | expect(getABC().first).toBe('a');
18 | });
19 |
20 | test('String.last', () => {
21 | expect(getEmpty().last).toBe(undefined);
22 | expect(getABC().last).toBe('c');
23 | });
24 |
25 | test('String.randomElement()', () => {
26 | expect(getEmpty().randomElement()).toBe(undefined);
27 | expect(typeof getABC().randomElement()).toBe('string');
28 | });
29 |
30 | test('String.map()', () => {
31 | const mapped = getABC().map(() => 'q');
32 | expect(mapped).toBe('qqq');
33 | });
34 |
35 | test('String.compactMap()', () => {
36 | const mapped = getABC().compactMap(letter => {
37 | return letter === 'a' ? 'q' : undefined;
38 | });
39 | expect(mapped).toBe('q');
40 | });
41 |
42 | test('String.forEach()', () => {
43 | let i = 0;
44 | getABC().forEach(() => i++);
45 | expect(i).toBe(getABC().length);
46 | });
47 |
48 | test('String.reduce()', () => {
49 | const aCount = getABC().reduce((numberOfAs, letter) => {
50 | return letter === 'a' ? numberOfAs + 1 : numberOfAs;
51 | }, 0);
52 | expect(aCount).toBe(1);
53 | });
54 |
55 | test('String.sorted()', () => {
56 | const shuffled = 'cab';
57 | expect(shuffled.sorted()).toBe('abc');
58 | });
59 |
60 | test('String.reversed()', () => {
61 | expect(getABC().reversed()).toBe('cba');
62 | });
63 |
64 | test('String.shuffled() exists', () => {
65 | expect(getABC().shuffled).not.toBe(undefined);
66 | });
67 |
68 | test('String.startIndex', () => {
69 | expect(getEmpty().startIndex).toBe(undefined);
70 | expect(getABC().startIndex).toBe(0);
71 | });
72 |
73 | test('String.endIndex', () => {
74 | expect(getEmpty().endIndex).toBe(undefined);
75 | expect(getABC().endIndex).toBe(2);
76 | });
77 |
78 | test('String.prefix', () => {
79 | const letters = 'aabbcc';
80 | const leadingAs = letters.prefix(char => char === 'a');
81 | expect(leadingAs.length).toBe(2);
82 | });
83 |
--------------------------------------------------------------------------------
/tests/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rootDir: "../",
3 | roots: [
4 | "/src",
5 | "/tests"
6 | ],
7 | testMatch: [
8 | "**/*.test.ts"
9 | ],
10 | transform: {
11 | "^.+\\.ts$": "ts-jest"
12 | },
13 | clearMocks: true,
14 | coverageProvider: "v8",
15 | testEnvironment: "node"
16 | };
17 |
--------------------------------------------------------------------------------