@20syldev/api 4.8.0 → 5.0.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 (126) hide show
  1. package/README.md +93 -55
  2. package/dist/config/versions.js +16 -0
  3. package/dist/config/versions.js.map +1 -1
  4. package/dist/constants.js +5 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/modules/v5/chart.js +379 -0
  7. package/dist/modules/v5/chart.js.map +1 -0
  8. package/dist/modules/v5/evaluate.js +281 -0
  9. package/dist/modules/v5/evaluate.js.map +1 -0
  10. package/dist/modules/v5/matrix.js +238 -0
  11. package/dist/modules/v5/matrix.js.map +1 -0
  12. package/dist/modules/v5.js +5 -0
  13. package/dist/modules/v5.js.map +1 -0
  14. package/dist/routes/get.js +191 -162
  15. package/dist/routes/get.js.map +1 -1
  16. package/dist/routes/post.js +69 -0
  17. package/dist/routes/post.js.map +1 -1
  18. package/package.json +4 -2
  19. package/recap.md +98 -0
  20. package/.github/FUNDING.yml +0 -1
  21. package/.prettierrc +0 -7
  22. package/eslint.config.js +0 -24
  23. package/robots.txt +0 -74
  24. package/src/app.ts +0 -53
  25. package/src/config/env.ts +0 -87
  26. package/src/config/plans.ts +0 -61
  27. package/src/config/versions.ts +0 -121
  28. package/src/constants.ts +0 -61
  29. package/src/favicon.ico +0 -0
  30. package/src/middleware/cors.ts +0 -23
  31. package/src/middleware/error.ts +0 -16
  32. package/src/middleware/json.ts +0 -9
  33. package/src/middleware/logger.ts +0 -32
  34. package/src/middleware/ratelimit.ts +0 -89
  35. package/src/middleware/version.ts +0 -40
  36. package/src/modules/v3/algorithms.js +0 -218
  37. package/src/modules/v3/captcha.js +0 -60
  38. package/src/modules/v3/chat.js +0 -111
  39. package/src/modules/v3/color.js +0 -60
  40. package/src/modules/v3/convert.js +0 -39
  41. package/src/modules/v3/domain.js +0 -86
  42. package/src/modules/v3/hash.js +0 -16
  43. package/src/modules/v3/hyperplanning.js +0 -56
  44. package/src/modules/v3/levenshtein.js +0 -46
  45. package/src/modules/v3/personal.js +0 -139
  46. package/src/modules/v3/qrcode.js +0 -20
  47. package/src/modules/v3/tic_tac_toe.js +0 -242
  48. package/src/modules/v3/time.js +0 -98
  49. package/src/modules/v3/token.js +0 -54
  50. package/src/modules/v3/username.js +0 -94
  51. package/src/modules/v3/utils.js +0 -77
  52. package/src/modules/v3.js +0 -15
  53. package/src/modules/v4/address.ts +0 -412
  54. package/src/modules/v4/agent.ts +0 -104
  55. package/src/modules/v4/algorithms.ts +0 -276
  56. package/src/modules/v4/captcha.ts +0 -114
  57. package/src/modules/v4/chat.ts +0 -130
  58. package/src/modules/v4/color.ts +0 -75
  59. package/src/modules/v4/convert.ts +0 -127
  60. package/src/modules/v4/cron.ts +0 -221
  61. package/src/modules/v4/dice.ts +0 -46
  62. package/src/modules/v4/domain.ts +0 -86
  63. package/src/modules/v4/encode.ts +0 -241
  64. package/src/modules/v4/geo.ts +0 -64
  65. package/src/modules/v4/hash.ts +0 -32
  66. package/src/modules/v4/hyperplanning.ts +0 -93
  67. package/src/modules/v4/ip.ts +0 -134
  68. package/src/modules/v4/levenshtein.ts +0 -46
  69. package/src/modules/v4/palette.ts +0 -71
  70. package/src/modules/v4/password.ts +0 -379
  71. package/src/modules/v4/personal.ts +0 -140
  72. package/src/modules/v4/placeholder.ts +0 -197
  73. package/src/modules/v4/qrcode.ts +0 -121
  74. package/src/modules/v4/regex.ts +0 -67
  75. package/src/modules/v4/statistics.ts +0 -62
  76. package/src/modules/v4/text.ts +0 -330
  77. package/src/modules/v4/tic_tac_toe.ts +0 -264
  78. package/src/modules/v4/time.ts +0 -149
  79. package/src/modules/v4/token.ts +0 -53
  80. package/src/modules/v4/username.ts +0 -94
  81. package/src/modules/v4/validate.ts +0 -76
  82. package/src/modules/v4.ts +0 -29
  83. package/src/routes/delete.ts +0 -72
  84. package/src/routes/get.ts +0 -856
  85. package/src/routes/index.ts +0 -59
  86. package/src/routes/patch.ts +0 -45
  87. package/src/routes/post.ts +0 -209
  88. package/src/storage/index.ts +0 -16
  89. package/src/types/express.d.ts +0 -17
  90. package/src/types/storage.ts +0 -48
  91. package/src/utils/colors.ts +0 -91
  92. package/src/utils/helpers.ts +0 -90
  93. package/src/utils/response.ts +0 -28
  94. package/tests/integration/api.test.ts +0 -772
  95. package/tests/tsconfig.json +0 -3
  96. package/tests/unit/address.test.ts +0 -84
  97. package/tests/unit/agent.test.ts +0 -113
  98. package/tests/unit/algorithms.test.ts +0 -121
  99. package/tests/unit/captcha.test.ts +0 -62
  100. package/tests/unit/chat.test.ts +0 -54
  101. package/tests/unit/color.test.ts +0 -54
  102. package/tests/unit/convert.test.ts +0 -68
  103. package/tests/unit/cron.test.ts +0 -110
  104. package/tests/unit/dice.test.ts +0 -58
  105. package/tests/unit/domain.test.ts +0 -48
  106. package/tests/unit/encode.test.ts +0 -109
  107. package/tests/unit/geo.test.ts +0 -46
  108. package/tests/unit/hash.test.ts +0 -45
  109. package/tests/unit/hyperplanning.test.ts +0 -97
  110. package/tests/unit/ip.test.ts +0 -140
  111. package/tests/unit/levenshtein.test.ts +0 -35
  112. package/tests/unit/palette.test.ts +0 -54
  113. package/tests/unit/password.test.ts +0 -86
  114. package/tests/unit/personal.test.ts +0 -66
  115. package/tests/unit/placeholder.test.ts +0 -58
  116. package/tests/unit/qrcode.test.ts +0 -83
  117. package/tests/unit/regex.test.ts +0 -95
  118. package/tests/unit/statistics.test.ts +0 -58
  119. package/tests/unit/text.test.ts +0 -108
  120. package/tests/unit/tic_tac_toe.test.ts +0 -56
  121. package/tests/unit/time.test.ts +0 -115
  122. package/tests/unit/token.test.ts +0 -62
  123. package/tests/unit/username.test.ts +0 -35
  124. package/tests/unit/validate.test.ts +0 -72
  125. package/tsconfig.json +0 -17
  126. 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
- }