@20syldev/api 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/package.json +5 -4
- package/src/app.ts +7 -3
- package/src/config/versions.ts +25 -10
- package/src/constants.ts +19 -0
- package/src/modules/v4/algorithms.ts +43 -1
- package/src/modules/v4/dice.ts +39 -0
- package/src/modules/v4/encode.ts +170 -0
- package/src/modules/v4/statistics.ts +55 -0
- package/src/modules/v4/text.ts +300 -0
- package/src/modules/v4/validate.ts +55 -0
- package/src/modules/v4.ts +5 -0
- package/src/routes/get.ts +134 -0
- package/src/routes/index.ts +2 -2
- package/tests/integration/api.test.ts +450 -0
- package/tests/tsconfig.json +3 -0
- package/tests/unit/algorithms.test.ts +120 -0
- package/tests/unit/color.test.ts +33 -0
- package/tests/unit/convert.test.ts +42 -0
- package/tests/unit/dice.test.ts +57 -0
- package/tests/unit/domain.test.ts +47 -0
- package/tests/unit/encode.test.ts +108 -0
- package/tests/unit/hash.test.ts +37 -0
- package/tests/unit/hyperplanning.test.ts +77 -0
- package/tests/unit/levenshtein.test.ts +34 -0
- package/tests/unit/personal.test.ts +65 -0
- package/tests/unit/statistics.test.ts +57 -0
- package/tests/unit/text.test.ts +107 -0
- package/tests/unit/time.test.ts +67 -0
- package/tests/unit/token.test.ts +61 -0
- package/tests/unit/username.test.ts +34 -0
- package/tests/unit/validate.test.ts +71 -0
- package/tsconfig.test.json +9 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://api.sylvain.sh"><img src="https://api.sylvain.sh/favicon.ico" alt="Logo" width="25%" height="auto"/></a>
|
|
3
3
|
|
|
4
4
|
# API Personnelle
|
|
5
|
-
[](https://github.com/20syldev/api/releases/latest)
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -32,10 +32,10 @@ Pour démarrer un serveur local avec tous les endpoints :
|
|
|
32
32
|
$ npm run build && npm start
|
|
33
33
|
```
|
|
34
34
|
```console
|
|
35
|
-
> @20syldev/api@4.
|
|
35
|
+
> @20syldev/api@4.2.0 build
|
|
36
36
|
> tsc
|
|
37
37
|
|
|
38
|
-
> @20syldev/api@4.
|
|
38
|
+
> @20syldev/api@4.2.0 start
|
|
39
39
|
> node dist/app.js
|
|
40
40
|
|
|
41
41
|
API is running on
|
|
@@ -93,16 +93,21 @@ import {
|
|
|
93
93
|
chat, // Système de chat temporaire
|
|
94
94
|
color, // Génération de couleurs aléatoires
|
|
95
95
|
convert, // Conversions d'unités
|
|
96
|
+
dice, // Lanceur de dés RPG
|
|
96
97
|
domain, // Informations de domaine aléatoires
|
|
98
|
+
encode, // Encodage / décodage (base64, morse, rot13, caesar, binaire)
|
|
97
99
|
hash, // Hachage de texte
|
|
98
100
|
hyperplanning, // Analyse de calendriers
|
|
99
101
|
levenshtein, // Distance entre chaînes
|
|
100
102
|
personal, // Informations personnelles aléatoires
|
|
101
103
|
qrcode, // Génération de QR codes
|
|
104
|
+
statistics, // Statistiques descriptives
|
|
102
105
|
tic_tac_toe, // Jeu de morpion
|
|
103
106
|
time, // Informations temporelles
|
|
107
|
+
text, // Utilitaires texte (slug, stats, lorem, nombre en lettres)
|
|
104
108
|
token, // Génération de jetons sécurisés
|
|
105
|
-
username
|
|
109
|
+
username, // Génération de noms d'utilisateur
|
|
110
|
+
validate // Validation (Luhn, IBAN, email)
|
|
106
111
|
} from '@20syldev/api/v4';
|
|
107
112
|
```
|
|
108
113
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.2.0",
|
|
3
3
|
"name": "@20syldev/api",
|
|
4
4
|
"description": "Node.js API with multiple features. Check the documentation at https://docs.sylvain.sh",
|
|
5
5
|
"main": "dist/app.js",
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
"dev": "tsx watch src/app.ts",
|
|
9
9
|
"start": "node dist/app.js",
|
|
10
10
|
"build": "tsc",
|
|
11
|
-
"check": "tsc --noEmit && eslint src/",
|
|
12
|
-
"format": "prettier --write src/ && eslint --fix src/",
|
|
11
|
+
"check": "tsc --noEmit -p tsconfig.test.json && eslint src/ tests/",
|
|
12
|
+
"format": "prettier --write src/ tests/ && eslint --fix src/ tests/",
|
|
13
|
+
"test": "NODE_ENV=test node --import tsx --test --test-force-exit tests/unit/*.test.ts tests/integration/*.test.ts",
|
|
13
14
|
"upgrade:minor": "npm upgrade",
|
|
14
15
|
"upgrade:major": "npx npm-check-updates -u && npm install"
|
|
15
16
|
},
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"@eslint/js": "^9.24.0",
|
|
38
39
|
"@types/cors": "^2.8.17",
|
|
39
40
|
"@types/express": "^5.0.0",
|
|
40
|
-
"@types/node": "^22.
|
|
41
|
+
"@types/node": "^22.19.17",
|
|
41
42
|
"@types/qrcode": "^1.5.6",
|
|
42
43
|
"@types/uuid": "^10.0.0",
|
|
43
44
|
"eslint": "^9.24.0",
|
package/src/app.ts
CHANGED
|
@@ -39,6 +39,10 @@ app.use(getRoutes);
|
|
|
39
39
|
app.use(postRoutes);
|
|
40
40
|
|
|
41
41
|
// Start server
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
42
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
43
|
+
app.listen(env.PORT, () =>
|
|
44
|
+
console.log(`API is running on\n - http://127.0.0.1:${env.PORT}\n - http://localhost:${env.PORT}`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default app;
|
package/src/config/versions.ts
CHANGED
|
@@ -11,10 +11,22 @@ export interface VersionConfig {
|
|
|
11
11
|
endpoints: {
|
|
12
12
|
get: Endpoint[];
|
|
13
13
|
post: Endpoint[];
|
|
14
|
+
patch?: Endpoint[];
|
|
15
|
+
delete?: Endpoint[];
|
|
14
16
|
};
|
|
15
17
|
modules: typeof apiv3 | typeof apiv4;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Merges a base list of endpoints with additions, deduplicating by `name`.
|
|
22
|
+
* If an addition has the same name as a base entry, it overrides it.
|
|
23
|
+
*/
|
|
24
|
+
const merge = (base: Endpoint[], additions: Endpoint[]): Endpoint[] => {
|
|
25
|
+
const map = new Map(base.map((e) => [e.name, e]));
|
|
26
|
+
for (const e of additions) map.set(e.name, e);
|
|
27
|
+
return [...map.values()];
|
|
28
|
+
};
|
|
29
|
+
|
|
18
30
|
const v1 = {
|
|
19
31
|
get: [
|
|
20
32
|
{ name: 'algorithms', path: '/algorithms?method={algorithm}&value={value}(&value2={value2})' },
|
|
@@ -32,9 +44,8 @@ const v1 = {
|
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
const v2 = {
|
|
35
|
-
get:
|
|
36
|
-
post: [
|
|
37
|
-
...v1.post,
|
|
47
|
+
get: merge(v1.get, [{ name: 'chat', path: '/chat' }]),
|
|
48
|
+
post: merge(v1.post, [
|
|
38
49
|
{
|
|
39
50
|
name: 'chat',
|
|
40
51
|
children: {
|
|
@@ -51,24 +62,28 @@ const v2 = {
|
|
|
51
62
|
list: '/tic-tac-toe/list',
|
|
52
63
|
} as Record<string, string>,
|
|
53
64
|
},
|
|
54
|
-
|
|
55
|
-
],
|
|
65
|
+
]),
|
|
56
66
|
};
|
|
57
67
|
|
|
58
68
|
const v3 = {
|
|
59
|
-
get: [
|
|
60
|
-
...v2.get,
|
|
69
|
+
get: merge(v2.get, [
|
|
61
70
|
{ name: 'levenshtein', path: '/levenshtein?str1={string}&str2={string}' },
|
|
62
71
|
{
|
|
63
72
|
name: 'time',
|
|
64
73
|
path: '/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})',
|
|
65
74
|
},
|
|
66
|
-
],
|
|
67
|
-
post:
|
|
75
|
+
]),
|
|
76
|
+
post: merge(v2.post, [{ name: 'hyperplanning', path: '/hyperplanning' }]),
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
const v4 = {
|
|
71
|
-
get:
|
|
80
|
+
get: merge(v3.get, [
|
|
81
|
+
{ name: 'dice', path: '/dice?roll={NdX+M}' },
|
|
82
|
+
{ name: 'encode', path: '/encode?method={method}&text={text}(&shift={shift})' },
|
|
83
|
+
{ name: 'statistics', path: '/statistics?values={n1,n2,n3,...}' },
|
|
84
|
+
{ name: 'text', path: '/text?method={method}(&value={value}&type={type}&count={count}&lang={lang})' },
|
|
85
|
+
{ name: 'validate', path: '/validate?type={type}&value={value}' },
|
|
86
|
+
]),
|
|
72
87
|
post: [...v3.post],
|
|
73
88
|
};
|
|
74
89
|
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
export const APP_VERSION = pkg.version;
|
|
1
4
|
export const MAX_LOG_ENTRIES = 1000;
|
|
2
5
|
export const RATE_LIMIT_WINDOW = 10_000;
|
|
3
6
|
export const RATE_LIMIT_MAX = 50;
|
|
@@ -26,3 +29,19 @@ export const STATUS_MESSAGES: Record<number, string> = {
|
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export const START_TIME = Date.now();
|
|
32
|
+
|
|
33
|
+
export const ROMAN_VALUES: [number, string][] = [
|
|
34
|
+
[1000, 'M'],
|
|
35
|
+
[900, 'CM'],
|
|
36
|
+
[500, 'D'],
|
|
37
|
+
[400, 'CD'],
|
|
38
|
+
[100, 'C'],
|
|
39
|
+
[90, 'XC'],
|
|
40
|
+
[50, 'L'],
|
|
41
|
+
[40, 'XL'],
|
|
42
|
+
[10, 'X'],
|
|
43
|
+
[9, 'IX'],
|
|
44
|
+
[5, 'V'],
|
|
45
|
+
[4, 'IV'],
|
|
46
|
+
[1, 'I'],
|
|
47
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_FACTORIAL, MAX_GCD_VALUE, MAX_PRIME_LIST, MAX_STRING_LENGTH } from '../../constants.js';
|
|
1
|
+
import { MAX_FACTORIAL, MAX_GCD_VALUE, MAX_PRIME_LIST, MAX_STRING_LENGTH, ROMAN_VALUES } from '../../constants.js';
|
|
2
2
|
|
|
3
3
|
export function anagram(value: string, value2: string): boolean {
|
|
4
4
|
if (!value) throw new Error('First value is required');
|
|
@@ -153,3 +153,45 @@ export function reverse(value: string): string {
|
|
|
153
153
|
|
|
154
154
|
return value.split('').reverse().join('');
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export function roman(value: string): number | string {
|
|
158
|
+
if (!value) throw new Error('A value is required');
|
|
159
|
+
|
|
160
|
+
const num = Number(value);
|
|
161
|
+
if (!isNaN(num)) {
|
|
162
|
+
if (!Number.isInteger(num)) throw new Error('Value must be an integer');
|
|
163
|
+
if (num < 1 || num > 3999) throw new Error('Number must be between 1 and 3999');
|
|
164
|
+
|
|
165
|
+
let result = '';
|
|
166
|
+
let n = num;
|
|
167
|
+
for (const [val, sym] of ROMAN_VALUES) {
|
|
168
|
+
while (n >= val) {
|
|
169
|
+
result += sym;
|
|
170
|
+
n -= val;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof value !== 'string') throw new Error('Value must be a number or a Roman numeral');
|
|
177
|
+
const upper = value.toUpperCase();
|
|
178
|
+
if (!/^[MDCLXVI]+$/.test(upper)) throw new Error('Invalid Roman numeral');
|
|
179
|
+
|
|
180
|
+
let result = 0;
|
|
181
|
+
let i = 0;
|
|
182
|
+
while (i < upper.length) {
|
|
183
|
+
const two = upper.slice(i, i + 2);
|
|
184
|
+
const match = ROMAN_VALUES.find(([, sym]) => sym === two);
|
|
185
|
+
if (match) {
|
|
186
|
+
result += match[0];
|
|
187
|
+
i += 2;
|
|
188
|
+
} else {
|
|
189
|
+
const one = ROMAN_VALUES.find(([, sym]) => sym === upper[i]);
|
|
190
|
+
if (!one) throw new Error('Invalid Roman numeral');
|
|
191
|
+
result += one[0];
|
|
192
|
+
i += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface DiceResult {
|
|
2
|
+
roll: string;
|
|
3
|
+
count: number;
|
|
4
|
+
sides: number;
|
|
5
|
+
modifier: number;
|
|
6
|
+
results: number[];
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function dice(roll: string): DiceResult {
|
|
11
|
+
if (!roll) throw new Error('A roll notation is required (e.g. 2d6+3)');
|
|
12
|
+
if (typeof roll !== 'string') throw new Error('Roll must be a string');
|
|
13
|
+
|
|
14
|
+
const match = /^(\d*)d(\d+)([+-]\d+)?$/i.exec(roll.trim().replace(/\s+/g, '+'));
|
|
15
|
+
if (!match) throw new Error('Invalid notation. Use NdX or NdX+M (e.g. 2d6+3)');
|
|
16
|
+
|
|
17
|
+
const count = match[1] ? parseInt(match[1], 10) : 1;
|
|
18
|
+
const sides = parseInt(match[2]!, 10);
|
|
19
|
+
const modifier = match[3] ? parseInt(match[3], 10) : 0;
|
|
20
|
+
|
|
21
|
+
if (count < 1 || count > 100) throw new Error('Number of dice must be between 1 and 100');
|
|
22
|
+
if (sides < 2 || sides > 1000) throw new Error('Number of sides must be between 2 and 1000');
|
|
23
|
+
|
|
24
|
+
const results: number[] = [];
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
results.push(1 + Math.floor(Math.random() * sides));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const total = results.reduce((a, b) => a + b, 0) + modifier;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
roll: `${count}d${sides}${modifier ? (modifier > 0 ? `+${modifier}` : modifier) : ''}`,
|
|
33
|
+
count,
|
|
34
|
+
sides,
|
|
35
|
+
modifier,
|
|
36
|
+
results,
|
|
37
|
+
total,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { MAX_STRING_LENGTH } from '../../constants.js';
|
|
2
|
+
|
|
3
|
+
const MORSE_TABLE: Record<string, string> = {
|
|
4
|
+
A: '.-',
|
|
5
|
+
B: '-...',
|
|
6
|
+
C: '-.-.',
|
|
7
|
+
D: '-..',
|
|
8
|
+
E: '.',
|
|
9
|
+
F: '..-.',
|
|
10
|
+
G: '--.',
|
|
11
|
+
H: '....',
|
|
12
|
+
I: '..',
|
|
13
|
+
J: '.---',
|
|
14
|
+
K: '-.-',
|
|
15
|
+
L: '.-..',
|
|
16
|
+
M: '--',
|
|
17
|
+
N: '-.',
|
|
18
|
+
O: '---',
|
|
19
|
+
P: '.--.',
|
|
20
|
+
Q: '--.-',
|
|
21
|
+
R: '.-.',
|
|
22
|
+
S: '...',
|
|
23
|
+
T: '-',
|
|
24
|
+
U: '..-',
|
|
25
|
+
V: '...-',
|
|
26
|
+
W: '.--',
|
|
27
|
+
X: '-..-',
|
|
28
|
+
Y: '-.--',
|
|
29
|
+
Z: '--..',
|
|
30
|
+
'0': '-----',
|
|
31
|
+
'1': '.----',
|
|
32
|
+
'2': '..---',
|
|
33
|
+
'3': '...--',
|
|
34
|
+
'4': '....-',
|
|
35
|
+
'5': '.....',
|
|
36
|
+
'6': '-....',
|
|
37
|
+
'7': '--...',
|
|
38
|
+
'8': '---..',
|
|
39
|
+
'9': '----.',
|
|
40
|
+
'.': '.-.-.-',
|
|
41
|
+
',': '--..--',
|
|
42
|
+
'?': '..--..',
|
|
43
|
+
"'": '.----.',
|
|
44
|
+
'!': '-.-.--',
|
|
45
|
+
'/': '-..-.',
|
|
46
|
+
'(': '-.--.',
|
|
47
|
+
')': '-.--.-',
|
|
48
|
+
'&': '.-...',
|
|
49
|
+
':': '---...',
|
|
50
|
+
';': '-.-.-.',
|
|
51
|
+
'=': '-...-',
|
|
52
|
+
'+': '.-.-.',
|
|
53
|
+
'-': '-....-',
|
|
54
|
+
_: '..--.-',
|
|
55
|
+
'"': '.-..-.',
|
|
56
|
+
$: '...-..-',
|
|
57
|
+
'@': '.--.-.',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const MORSE_REVERSE: Record<string, string> = Object.fromEntries(Object.entries(MORSE_TABLE).map(([k, v]) => [v, k]));
|
|
61
|
+
|
|
62
|
+
function checkText(value: string): void {
|
|
63
|
+
if (!value) throw new Error('A value is required');
|
|
64
|
+
if (typeof value !== 'string') throw new Error('Value must be a string');
|
|
65
|
+
if (value.length > MAX_STRING_LENGTH)
|
|
66
|
+
throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function base64encode(value: string): string {
|
|
70
|
+
checkText(value);
|
|
71
|
+
return Buffer.from(value, 'utf-8').toString('base64');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function base64decode(value: string): string {
|
|
75
|
+
checkText(value);
|
|
76
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) throw new Error('Invalid Base64 string');
|
|
77
|
+
return Buffer.from(value, 'base64').toString('utf-8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function urlencode(value: string): string {
|
|
81
|
+
checkText(value);
|
|
82
|
+
return encodeURIComponent(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function urldecode(value: string): string {
|
|
86
|
+
checkText(value);
|
|
87
|
+
try {
|
|
88
|
+
return decodeURIComponent(value);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error('Invalid URL-encoded string');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function morse(value: string): string {
|
|
95
|
+
checkText(value);
|
|
96
|
+
return value
|
|
97
|
+
.toUpperCase()
|
|
98
|
+
.split(' ')
|
|
99
|
+
.map((word) =>
|
|
100
|
+
word
|
|
101
|
+
.split('')
|
|
102
|
+
.map((c) => {
|
|
103
|
+
if (c === '\n' || c === '\t') return '';
|
|
104
|
+
const code = MORSE_TABLE[c];
|
|
105
|
+
if (!code) throw new Error(`Unsupported character in Morse code: '${c}'`);
|
|
106
|
+
return code;
|
|
107
|
+
})
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' '),
|
|
110
|
+
)
|
|
111
|
+
.join(' / ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function unmorse(value: string): string {
|
|
115
|
+
checkText(value);
|
|
116
|
+
return value
|
|
117
|
+
.split(' / ')
|
|
118
|
+
.map((word) =>
|
|
119
|
+
word
|
|
120
|
+
.split(' ')
|
|
121
|
+
.map((code) => {
|
|
122
|
+
if (!code) return '';
|
|
123
|
+
const char = MORSE_REVERSE[code];
|
|
124
|
+
if (!char) throw new Error(`Unsupported Morse sequence: '${code}'`);
|
|
125
|
+
return char;
|
|
126
|
+
})
|
|
127
|
+
.join(''),
|
|
128
|
+
)
|
|
129
|
+
.join(' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function rot13(value: string): string {
|
|
133
|
+
checkText(value);
|
|
134
|
+
return value.replace(/[a-zA-Z]/g, (c) => {
|
|
135
|
+
const base = c <= 'Z' ? 65 : 97;
|
|
136
|
+
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function caesar(value: string, shift: string): string {
|
|
141
|
+
checkText(value);
|
|
142
|
+
const n = Number(shift);
|
|
143
|
+
if (isNaN(n)) throw new Error('Shift must be a number');
|
|
144
|
+
const s = ((n % 26) + 26) % 26;
|
|
145
|
+
return value.replace(/[a-zA-Z]/g, (c) => {
|
|
146
|
+
const base = c <= 'Z' ? 65 : 97;
|
|
147
|
+
return String.fromCharCode(((c.charCodeAt(0) - base + s) % 26) + base);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function binary(value: string): string {
|
|
152
|
+
checkText(value);
|
|
153
|
+
return value
|
|
154
|
+
.split('')
|
|
155
|
+
.map((c) => c.charCodeAt(0).toString(2).padStart(8, '0'))
|
|
156
|
+
.join(' ');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function unbinary(value: string): string {
|
|
160
|
+
checkText(value);
|
|
161
|
+
if (!/^[01\s]+$/.test(value)) throw new Error('Invalid binary string');
|
|
162
|
+
return value
|
|
163
|
+
.trim()
|
|
164
|
+
.split(/\s+/)
|
|
165
|
+
.map((b) => {
|
|
166
|
+
if (b.length !== 8) throw new Error('Each binary group must contain 8 bits');
|
|
167
|
+
return String.fromCharCode(parseInt(b, 2));
|
|
168
|
+
})
|
|
169
|
+
.join('');
|
|
170
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MAX_STRING_LENGTH } from '../../constants.js';
|
|
2
|
+
|
|
3
|
+
export interface StatisticsResult {
|
|
4
|
+
count: number;
|
|
5
|
+
sum: number;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
range: number;
|
|
9
|
+
mean: number;
|
|
10
|
+
median: number;
|
|
11
|
+
mode: number[];
|
|
12
|
+
variance: number;
|
|
13
|
+
stddev: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function statistics(values: string): StatisticsResult {
|
|
17
|
+
if (!values) throw new Error('A list of values is required');
|
|
18
|
+
if (typeof values !== 'string') throw new Error('Values must be a comma-separated string');
|
|
19
|
+
|
|
20
|
+
const arr = values.split(',').map((v) => Number(v.trim()));
|
|
21
|
+
|
|
22
|
+
if (arr.some(isNaN)) throw new Error('Values must contain only numbers');
|
|
23
|
+
if (arr.length < 1) throw new Error('At least one value is required');
|
|
24
|
+
if (arr.length > MAX_STRING_LENGTH) throw new Error(`Cannot process more than ${MAX_STRING_LENGTH} values`);
|
|
25
|
+
|
|
26
|
+
const count = arr.length;
|
|
27
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
28
|
+
const min = Math.min(...arr);
|
|
29
|
+
const max = Math.max(...arr);
|
|
30
|
+
const mean = sum / count;
|
|
31
|
+
|
|
32
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
33
|
+
const median = count % 2 === 0 ? (sorted[count / 2 - 1]! + sorted[count / 2]!) / 2 : sorted[Math.floor(count / 2)]!;
|
|
34
|
+
|
|
35
|
+
const counts = new Map<number, number>();
|
|
36
|
+
for (const n of arr) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
37
|
+
const maxCount = Math.max(...counts.values());
|
|
38
|
+
const mode = maxCount === 1 ? [] : [...counts.entries()].filter(([, c]) => c === maxCount).map(([n]) => n);
|
|
39
|
+
|
|
40
|
+
const variance = arr.reduce((acc, n) => acc + (n - mean) ** 2, 0) / count;
|
|
41
|
+
const stddev = Math.sqrt(variance);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
count,
|
|
45
|
+
sum: +sum.toFixed(6),
|
|
46
|
+
min,
|
|
47
|
+
max,
|
|
48
|
+
range: max - min,
|
|
49
|
+
mean: +mean.toFixed(6),
|
|
50
|
+
median: +median.toFixed(6),
|
|
51
|
+
mode,
|
|
52
|
+
variance: +variance.toFixed(6),
|
|
53
|
+
stddev: +stddev.toFixed(6),
|
|
54
|
+
};
|
|
55
|
+
}
|