@20syldev/api 4.8.0 → 4.9.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.
Files changed (109) hide show
  1. package/README.md +84 -47
  2. package/package.json +2 -1
  3. package/.github/FUNDING.yml +0 -1
  4. package/.prettierrc +0 -7
  5. package/eslint.config.js +0 -24
  6. package/robots.txt +0 -74
  7. package/src/app.ts +0 -53
  8. package/src/config/env.ts +0 -87
  9. package/src/config/plans.ts +0 -61
  10. package/src/config/versions.ts +0 -121
  11. package/src/constants.ts +0 -61
  12. package/src/favicon.ico +0 -0
  13. package/src/middleware/cors.ts +0 -23
  14. package/src/middleware/error.ts +0 -16
  15. package/src/middleware/json.ts +0 -9
  16. package/src/middleware/logger.ts +0 -32
  17. package/src/middleware/ratelimit.ts +0 -89
  18. package/src/middleware/version.ts +0 -40
  19. package/src/modules/v3/algorithms.js +0 -218
  20. package/src/modules/v3/captcha.js +0 -60
  21. package/src/modules/v3/chat.js +0 -111
  22. package/src/modules/v3/color.js +0 -60
  23. package/src/modules/v3/convert.js +0 -39
  24. package/src/modules/v3/domain.js +0 -86
  25. package/src/modules/v3/hash.js +0 -16
  26. package/src/modules/v3/hyperplanning.js +0 -56
  27. package/src/modules/v3/levenshtein.js +0 -46
  28. package/src/modules/v3/personal.js +0 -139
  29. package/src/modules/v3/qrcode.js +0 -20
  30. package/src/modules/v3/tic_tac_toe.js +0 -242
  31. package/src/modules/v3/time.js +0 -98
  32. package/src/modules/v3/token.js +0 -54
  33. package/src/modules/v3/username.js +0 -94
  34. package/src/modules/v3/utils.js +0 -77
  35. package/src/modules/v3.js +0 -15
  36. package/src/modules/v4/address.ts +0 -412
  37. package/src/modules/v4/agent.ts +0 -104
  38. package/src/modules/v4/algorithms.ts +0 -276
  39. package/src/modules/v4/captcha.ts +0 -114
  40. package/src/modules/v4/chat.ts +0 -130
  41. package/src/modules/v4/color.ts +0 -75
  42. package/src/modules/v4/convert.ts +0 -127
  43. package/src/modules/v4/cron.ts +0 -221
  44. package/src/modules/v4/dice.ts +0 -46
  45. package/src/modules/v4/domain.ts +0 -86
  46. package/src/modules/v4/encode.ts +0 -241
  47. package/src/modules/v4/geo.ts +0 -64
  48. package/src/modules/v4/hash.ts +0 -32
  49. package/src/modules/v4/hyperplanning.ts +0 -93
  50. package/src/modules/v4/ip.ts +0 -134
  51. package/src/modules/v4/levenshtein.ts +0 -46
  52. package/src/modules/v4/palette.ts +0 -71
  53. package/src/modules/v4/password.ts +0 -379
  54. package/src/modules/v4/personal.ts +0 -140
  55. package/src/modules/v4/placeholder.ts +0 -197
  56. package/src/modules/v4/qrcode.ts +0 -121
  57. package/src/modules/v4/regex.ts +0 -67
  58. package/src/modules/v4/statistics.ts +0 -62
  59. package/src/modules/v4/text.ts +0 -330
  60. package/src/modules/v4/tic_tac_toe.ts +0 -264
  61. package/src/modules/v4/time.ts +0 -149
  62. package/src/modules/v4/token.ts +0 -53
  63. package/src/modules/v4/username.ts +0 -94
  64. package/src/modules/v4/validate.ts +0 -76
  65. package/src/modules/v4.ts +0 -29
  66. package/src/routes/delete.ts +0 -72
  67. package/src/routes/get.ts +0 -856
  68. package/src/routes/index.ts +0 -59
  69. package/src/routes/patch.ts +0 -45
  70. package/src/routes/post.ts +0 -209
  71. package/src/storage/index.ts +0 -16
  72. package/src/types/express.d.ts +0 -17
  73. package/src/types/storage.ts +0 -48
  74. package/src/utils/colors.ts +0 -91
  75. package/src/utils/helpers.ts +0 -90
  76. package/src/utils/response.ts +0 -28
  77. package/tests/integration/api.test.ts +0 -772
  78. package/tests/tsconfig.json +0 -3
  79. package/tests/unit/address.test.ts +0 -84
  80. package/tests/unit/agent.test.ts +0 -113
  81. package/tests/unit/algorithms.test.ts +0 -121
  82. package/tests/unit/captcha.test.ts +0 -62
  83. package/tests/unit/chat.test.ts +0 -54
  84. package/tests/unit/color.test.ts +0 -54
  85. package/tests/unit/convert.test.ts +0 -68
  86. package/tests/unit/cron.test.ts +0 -110
  87. package/tests/unit/dice.test.ts +0 -58
  88. package/tests/unit/domain.test.ts +0 -48
  89. package/tests/unit/encode.test.ts +0 -109
  90. package/tests/unit/geo.test.ts +0 -46
  91. package/tests/unit/hash.test.ts +0 -45
  92. package/tests/unit/hyperplanning.test.ts +0 -97
  93. package/tests/unit/ip.test.ts +0 -140
  94. package/tests/unit/levenshtein.test.ts +0 -35
  95. package/tests/unit/palette.test.ts +0 -54
  96. package/tests/unit/password.test.ts +0 -86
  97. package/tests/unit/personal.test.ts +0 -66
  98. package/tests/unit/placeholder.test.ts +0 -58
  99. package/tests/unit/qrcode.test.ts +0 -83
  100. package/tests/unit/regex.test.ts +0 -95
  101. package/tests/unit/statistics.test.ts +0 -58
  102. package/tests/unit/text.test.ts +0 -108
  103. package/tests/unit/tic_tac_toe.test.ts +0 -56
  104. package/tests/unit/time.test.ts +0 -115
  105. package/tests/unit/token.test.ts +0 -62
  106. package/tests/unit/username.test.ts +0 -35
  107. package/tests/unit/validate.test.ts +0 -72
  108. package/tsconfig.json +0 -17
  109. package/tsconfig.test.json +0 -9
@@ -1,197 +0,0 @@
1
- import { normalizeColor } from '../../utils/colors.js';
2
-
3
- type AvatarShape = 'circle' | 'rounded' | 'square';
4
- type AnimateMode = 'shimmer' | 'pulse' | 'none';
5
-
6
- export interface PlaceholderOptions {
7
- width: number;
8
- height: number;
9
- bg?: string;
10
- color?: string;
11
- text?: string;
12
- rows?: number;
13
- avatar?: AvatarShape;
14
- lines?: number;
15
- animate?: AnimateMode;
16
- speed?: number;
17
- radius?: number;
18
- }
19
-
20
- function parseSize(value: string | undefined, name: string, def: number): number {
21
- if (value === undefined) return def;
22
- const n = Number(value);
23
- if (isNaN(n)) throw new Error(`${name} must be a number`);
24
- if (n < 1 || n > 4000) throw new Error(`${name} must be between 1 and 4000`);
25
- return Math.floor(n);
26
- }
27
-
28
- function escapeXml(str: string): string {
29
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
30
- }
31
-
32
- function generateImage(opts: PlaceholderOptions): string {
33
- const { width, height } = opts;
34
- const bg = normalizeColor(opts.bg, '#cccccc');
35
- const color = normalizeColor(opts.color, '#333333');
36
- const text = escapeXml(opts.text ?? `${width}\u00d7${height}`);
37
- const fontSize = Math.max(12, Math.min(width, height) / 8);
38
-
39
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
40
- <rect width="${width}" height="${height}" fill="${bg}" rx="4" />
41
- <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-weight="600" font-size="${fontSize}" fill="${color}">${text}</text>
42
- </svg>`;
43
- }
44
-
45
- function avatarShape(shape: AvatarShape, x: number, y: number, size: number): string {
46
- const r = size / 2;
47
- switch (shape) {
48
- case 'circle':
49
- return `<circle cx="${x + r}" cy="${y + r}" r="${r}" />`;
50
- case 'rounded':
51
- return `<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="${Math.round(size * 0.25)}" />`;
52
- case 'square':
53
- return `<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="2" />`;
54
- }
55
- }
56
-
57
- function animationDefs(mode: AnimateMode, bg: string, highlight: string, speed: number): string {
58
- if (mode === 'none') return '';
59
-
60
- if (mode === 'pulse') {
61
- return `<style>
62
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
63
- .sk { animation: pulse ${speed}s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
64
- </style>`;
65
- }
66
-
67
- return `<linearGradient id="g">
68
- <stop offset="0.1" stop-color="${bg}" />
69
- <stop offset="0.5" stop-color="${highlight}" />
70
- <stop offset="0.9" stop-color="${bg}" />
71
- <animateTransform attributeName="gradientTransform" type="translate" values="-2 0; 2 0" dur="${speed}s" repeatCount="indefinite" />
72
- </linearGradient>`;
73
- }
74
-
75
- function generateSkeleton(opts: PlaceholderOptions): string {
76
- const { width, height } = opts;
77
- const bg = normalizeColor(opts.bg, '#e5e5e5');
78
- const highlight = normalizeColor(opts.color, '#f5f5f5');
79
- const rows = Math.max(0, Math.min(20, opts.rows ?? 3));
80
- const avatar = opts.avatar;
81
- const animate = opts.animate ?? 'shimmer';
82
- const speed = opts.speed ?? 1.5;
83
- const rx = opts.radius ?? 4;
84
-
85
- const pad = Math.round(width * 0.06);
86
- const slots = rows + (avatar ? 4 : 0);
87
- const available = height - pad * 2;
88
- const lineH = Math.round(Math.max(8, (available / Math.max(1, slots)) * 0.65));
89
- const gap = Math.round(Math.max(6, (available / Math.max(1, slots)) * 0.35));
90
-
91
- const shapes: string[] = [];
92
- let y = pad;
93
-
94
- if (avatar) {
95
- const size = Math.round(Math.min(width * 0.14, height * 0.24, 48));
96
- shapes.push(avatarShape(avatar, pad, y, size));
97
-
98
- const lx = pad + size + gap;
99
- const lw = width - lx - pad;
100
- const count = Math.max(0, Math.min(2, opts.lines ?? 2));
101
-
102
- if (count === 2) {
103
- const nameY = Math.max(pad, Math.round(y + size / 2 - (lineH * 2 + gap) / 2));
104
- shapes.push(`<rect x="${lx}" y="${nameY}" width="${lw * 0.55}" height="${lineH}" rx="${rx}" />`);
105
- shapes.push(
106
- `<rect x="${lx}" y="${nameY + lineH + gap}" width="${lw * 0.35}" height="${lineH}" rx="${rx}" />`,
107
- );
108
- } else if (count === 1) {
109
- const nameY = Math.max(pad, Math.round(y + size / 2 - lineH / 2));
110
- shapes.push(`<rect x="${lx}" y="${nameY}" width="${lw * 0.55}" height="${lineH}" rx="${rx}" />`);
111
- }
112
-
113
- y += size + gap * 2;
114
- }
115
-
116
- for (let i = 0; i < rows; i++) {
117
- const ratio = i === rows - 1 && rows > 1 ? 0.55 : i % 2 === 0 ? 0.92 : 0.78;
118
- const w = Math.round((width - pad * 2) * ratio);
119
- shapes.push(`<rect x="${pad}" y="${y}" width="${w}" height="${lineH}" rx="${rx}" />`);
120
- y += lineH + gap;
121
- }
122
-
123
- const defs = animationDefs(animate, bg, highlight, speed);
124
- const fill = animate === 'shimmer' ? 'url(#g)' : bg;
125
- const cls = animate === 'pulse' ? ' class="sk"' : '';
126
-
127
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
128
- <defs>
129
- <clipPath id="c">
130
- ${shapes.join('\n ')}
131
- </clipPath>
132
- ${defs}
133
- </defs>
134
- <rect width="${width}" height="${height}" clip-path="url(#c)" fill="${fill}"${cls} />
135
- </svg>`;
136
- }
137
-
138
- export interface PlaceholderResult {
139
- type: string;
140
- contentType: string;
141
- body: string;
142
- }
143
-
144
- const AVATAR_SHAPES = new Set(['circle', 'rounded', 'square']);
145
- const ANIMATE_MODES = new Set(['shimmer', 'pulse', 'none']);
146
-
147
- function parseAvatar(value: string | undefined): AvatarShape | undefined {
148
- if (!value) return undefined;
149
- if (value === 'true' || value === '1') return 'circle';
150
- if (AVATAR_SHAPES.has(value)) return value as AvatarShape;
151
- throw new Error('avatar must be one of: circle, rounded, square');
152
- }
153
-
154
- function parseAnimate(value: string | undefined): AnimateMode {
155
- if (!value) return 'shimmer';
156
- if (ANIMATE_MODES.has(value)) return value as AnimateMode;
157
- throw new Error('animate must be one of: shimmer, pulse, none');
158
- }
159
-
160
- /**
161
- * Generates an SVG placeholder image or skeleton loader with configurable dimensions and style.
162
- *
163
- * @param type - Placeholder type: "image" or "skeleton"
164
- * @param query - Query parameters including width, height, bg, color, text, rows, avatar, animate, speed, and radius
165
- * @returns Object containing the SVG body, content type, and placeholder type
166
- * @throws Error if any parameter is invalid or out of range
167
- */
168
- export default function placeholder(type: string, query: Record<string, string | undefined>): PlaceholderResult {
169
- const speed = query.speed ? parseFloat(query.speed) : 1.5;
170
- if (isNaN(speed) || speed < 0.1 || speed > 10) throw new Error('speed must be between 0.1 and 10');
171
-
172
- const radius = query.radius ? parseInt(query.radius, 10) : 4;
173
- if (isNaN(radius) || radius < 0 || radius > 50) throw new Error('radius must be between 0 and 50');
174
-
175
- const opts: PlaceholderOptions = {
176
- width: parseSize(query.width, 'width', 800),
177
- height: parseSize(query.height, 'height', 600),
178
- bg: query.bg,
179
- color: query.color,
180
- text: query.text,
181
- rows: query.rows ? parseInt(query.rows, 10) : undefined,
182
- avatar: parseAvatar(query.avatar),
183
- lines: query.lines ? parseInt(query.lines, 10) : 2,
184
- animate: parseAnimate(query.animate),
185
- speed,
186
- radius,
187
- };
188
-
189
- switch (type) {
190
- case 'image':
191
- return { type, contentType: 'image/svg+xml', body: generateImage(opts) };
192
- case 'skeleton':
193
- return { type, contentType: 'image/svg+xml', body: generateSkeleton(opts) };
194
- default:
195
- throw new Error('Type must be one of: image, skeleton');
196
- }
197
- }
@@ -1,121 +0,0 @@
1
- import { createCanvas, loadImage } from 'canvas';
2
- import { toBuffer, toCanvas as qrToCanvas, toDataURL } from 'qrcode';
3
-
4
- import { MAX_QRCODE_LOGO_BYTES } from '../../constants.js';
5
- import { normalizeColor } from '../../utils/colors.js';
6
-
7
- export interface QRCodeOptions {
8
- url: string;
9
- size?: number;
10
- margin?: number;
11
- correction?: 'L' | 'M' | 'Q' | 'H';
12
- dark?: string;
13
- light?: string;
14
- icon?: string;
15
- iconSize?: number;
16
- iconPadding?: number;
17
- iconRadius?: number;
18
- format?: 'png' | 'base64';
19
- }
20
-
21
- export interface QRCodeResult {
22
- contentType: string;
23
- body: Buffer | string;
24
- }
25
-
26
- const CORRECTIONS = new Set(['L', 'M', 'Q', 'H']);
27
- const FORMATS = new Set(['png', 'base64']);
28
-
29
- function clamp(value: number | undefined, name: string, def: number, min: number, max: number): number {
30
- if (value === undefined) return def;
31
- if (isNaN(value)) throw new Error(`${name} must be a number`);
32
- if (value < min || value > max) throw new Error(`${name} must be between ${min} and ${max}`);
33
- return Math.floor(value);
34
- }
35
-
36
- async function fetchIcon(url: string): Promise<Buffer> {
37
- if (!/^https:\/\//i.test(url)) throw new Error('Icon URL must use HTTPS');
38
-
39
- const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
40
-
41
- if (!response.ok) throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
42
-
43
- const contentType = response.headers.get('content-type') ?? '';
44
- if (!contentType.startsWith('image/')) throw new Error('Icon URL must point to an image');
45
-
46
- const buffer = Buffer.from(await response.arrayBuffer());
47
- if (buffer.length > MAX_QRCODE_LOGO_BYTES) throw new Error('Icon must be under 2MB');
48
-
49
- return buffer;
50
- }
51
-
52
- /**
53
- * Generates a QR code image for a given URL, with optional icon overlay and color customization.
54
- *
55
- * @param options - QR code generation options including URL, size, margin, correction level, colors, and optional icon
56
- * @returns Object containing the QR code as a PNG buffer or Base64 string, and the content type
57
- * @throws Error if the URL is missing, any option is invalid, or the icon URL cannot be fetched
58
- */
59
- export default async function qrcode(options: QRCodeOptions): Promise<QRCodeResult> {
60
- const { url } = options;
61
- if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
62
-
63
- const format = options.format ?? 'png';
64
- if (!FORMATS.has(format)) throw new Error('format must be one of: png, base64');
65
-
66
- const size = clamp(options.size, 'size', 200, 50, 2000);
67
- const margin = clamp(options.margin, 'margin', 4, 0, 10);
68
- const dark = normalizeColor(options.dark, '#000000');
69
- const light = normalizeColor(options.light, '#ffffff');
70
-
71
- let correction = options.correction ?? 'M';
72
- if (!CORRECTIONS.has(correction)) throw new Error('correction must be one of: L, M, Q, H');
73
- if (options.icon) correction = 'H';
74
-
75
- const qrOpts = {
76
- width: size,
77
- margin,
78
- errorCorrectionLevel: correction as 'L' | 'M' | 'Q' | 'H',
79
- color: { dark, light },
80
- };
81
-
82
- if (!options.icon) {
83
- if (format === 'base64') {
84
- return { contentType: 'application/json', body: await toDataURL(url, qrOpts) };
85
- }
86
- return { contentType: 'image/png', body: await toBuffer(url, qrOpts) };
87
- }
88
-
89
- const canvas = createCanvas(size, size);
90
- await qrToCanvas(canvas as unknown as HTMLCanvasElement, url, qrOpts);
91
-
92
- const iconBuffer = await fetchIcon(options.icon);
93
- const iconImg = await loadImage(iconBuffer);
94
-
95
- const iconSize = clamp(options.iconSize, 'iconSize', Math.round(size * 0.2), 10, Math.round(size * 0.4));
96
- const iconPadding = clamp(options.iconPadding, 'iconPadding', 5, 0, 50);
97
- const maxRadius = Math.floor((iconSize + iconPadding * 2) / 2);
98
- const iconRadius = clamp(options.iconRadius, 'iconRadius', 0, 0, maxRadius);
99
-
100
- const ctx = canvas.getContext('2d');
101
- const totalSize = iconSize + iconPadding * 2;
102
- const x = (canvas.width - totalSize) / 2;
103
- const y = (canvas.height - totalSize) / 2;
104
-
105
- ctx.beginPath();
106
- ctx.roundRect(x, y, totalSize, totalSize, iconRadius);
107
- ctx.fillStyle = light;
108
- ctx.fill();
109
-
110
- ctx.save();
111
- ctx.beginPath();
112
- ctx.roundRect(x + iconPadding, y + iconPadding, iconSize, iconSize, Math.max(0, iconRadius - iconPadding));
113
- ctx.clip();
114
- ctx.drawImage(iconImg, x + iconPadding, y + iconPadding, iconSize, iconSize);
115
- ctx.restore();
116
-
117
- if (format === 'base64') {
118
- return { contentType: 'application/json', body: canvas.toDataURL('image/png') };
119
- }
120
- return { contentType: 'image/png', body: canvas.toBuffer('image/png') };
121
- }
@@ -1,67 +0,0 @@
1
- import { MAX_PATTERN_LENGTH, MAX_REGEX_MATCHES, MAX_STRING_LENGTH } from '../../constants.js';
2
-
3
- const VALID_FLAGS = new Set(['g', 'i', 'm', 's', 'u']);
4
-
5
- export interface RegexMatch {
6
- match: string;
7
- index: number;
8
- groups: string[];
9
- namedGroups: Record<string, string>;
10
- }
11
-
12
- export interface RegexResult {
13
- valid: boolean;
14
- pattern: string;
15
- flags: string;
16
- matches: RegexMatch[];
17
- count: number;
18
- }
19
-
20
- /**
21
- * Tests a regular expression pattern against a text and returns structured match results.
22
- *
23
- * @param pattern - The regex pattern to test (max 200 characters)
24
- * @param text - The text to match against (max 1000 characters)
25
- * @param flags - Optional regex flags; only g, i, m, s, u are accepted (g is always forced)
26
- * @returns Match results with groups and named groups, or { valid: false } if the pattern is invalid
27
- * @throws Error if pattern or text is missing or exceeds the maximum length
28
- */
29
- export default function regex(pattern: string, text: string, flags: string = 'g'): RegexResult {
30
- if (!pattern) {
31
- throw new Error('Please provide a pattern (?pattern={regex})');
32
- }
33
- if (!text) {
34
- throw new Error('Please provide a text (&text={string})');
35
- }
36
- if (pattern.length > MAX_PATTERN_LENGTH) {
37
- throw new Error(`Pattern must be under ${MAX_PATTERN_LENGTH} characters`);
38
- }
39
- if (text.length > MAX_STRING_LENGTH) {
40
- throw new Error(`Text must be under ${MAX_STRING_LENGTH} characters`);
41
- }
42
-
43
- const cleanedFlags = [...new Set(['g', ...flags.split('').filter((f) => VALID_FLAGS.has(f))])].join('');
44
-
45
- let re: RegExp;
46
- try {
47
- re = new RegExp(pattern, cleanedFlags);
48
- } catch {
49
- return { valid: false, pattern, flags: cleanedFlags, matches: [], count: 0 };
50
- }
51
-
52
- const matches: RegexMatch[] = [];
53
- let result: RegExpExecArray | null;
54
-
55
- while ((result = re.exec(text)) !== null && matches.length < MAX_REGEX_MATCHES) {
56
- matches.push({
57
- match: result[0],
58
- index: result.index,
59
- groups: result.slice(1).map((g) => g ?? ''),
60
- namedGroups: result.groups ? { ...result.groups } : {},
61
- });
62
- // Prevent infinite loop on zero-length matches
63
- if (result[0].length === 0) re.lastIndex++;
64
- }
65
-
66
- return { valid: true, pattern, flags: cleanedFlags, matches, count: matches.length };
67
- }
@@ -1,62 +0,0 @@
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
- /**
17
- * Computes descriptive statistics for a comma-separated list of numbers.
18
- *
19
- * @param values - Comma-separated numeric string (e.g. "1,2,3,4,5")
20
- * @returns Object containing count, sum, min, max, range, mean, median, mode, variance, and standard deviation
21
- * @throws Error if values is missing, contains non-numeric entries, or exceeds the maximum count
22
- */
23
- export default function statistics(values: string): StatisticsResult {
24
- if (!values) throw new Error('A list of values is required');
25
- if (typeof values !== 'string') throw new Error('Values must be a comma-separated string');
26
-
27
- const arr = values.split(',').map((v) => Number(v.trim()));
28
-
29
- if (arr.some(isNaN)) throw new Error('Values must contain only numbers');
30
- if (arr.length < 1) throw new Error('At least one value is required');
31
- if (arr.length > MAX_STRING_LENGTH) throw new Error(`Cannot process more than ${MAX_STRING_LENGTH} values`);
32
-
33
- const count = arr.length;
34
- const sum = arr.reduce((a, b) => a + b, 0);
35
- const min = Math.min(...arr);
36
- const max = Math.max(...arr);
37
- const mean = sum / count;
38
-
39
- const sorted = [...arr].sort((a, b) => a - b);
40
- const median = count % 2 === 0 ? (sorted[count / 2 - 1]! + sorted[count / 2]!) / 2 : sorted[Math.floor(count / 2)]!;
41
-
42
- const counts = new Map<number, number>();
43
- for (const n of arr) counts.set(n, (counts.get(n) ?? 0) + 1);
44
- const maxCount = Math.max(...counts.values());
45
- const mode = maxCount === 1 ? [] : [...counts.entries()].filter(([, c]) => c === maxCount).map(([n]) => n);
46
-
47
- const variance = arr.reduce((acc, n) => acc + (n - mean) ** 2, 0) / count;
48
- const stddev = Math.sqrt(variance);
49
-
50
- return {
51
- count,
52
- sum: +sum.toFixed(6),
53
- min,
54
- max,
55
- range: max - min,
56
- mean: +mean.toFixed(6),
57
- median: +median.toFixed(6),
58
- mode,
59
- variance: +variance.toFixed(6),
60
- stddev: +stddev.toFixed(6),
61
- };
62
- }