@base-web-kits/base-tools-ts 0.9.5 → 0.9.6

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.
@@ -0,0 +1,601 @@
1
+ /**
2
+ * 纯字母(不含空格与符号)。
3
+ * @param s 字符串
4
+ * @returns 是否为字母
5
+ * @example
6
+ * isLetter('abc') // true
7
+ * isLetter('123') // false
8
+ * isLetter('abc123') // false
9
+ */
10
+ export function isLetter(s: string) {
11
+ return /^[a-zA-Z]*$/.test(s);
12
+ }
13
+
14
+ /**
15
+ * 纯中文(不含空格与符号)。
16
+ * @param s 字符串
17
+ * @returns 是否为纯中文
18
+ * @example
19
+ * isChinese('你好') // true
20
+ */
21
+ export function isChinese(s: string) {
22
+ const v = String(s ?? '').trim();
23
+ return /^[\u4E00-\u9FA5]+$/.test(v);
24
+ }
25
+
26
+ /**
27
+ * 纯数字(非负整数,不含空格与符号)。
28
+ * @param s 字符串
29
+ * @returns 是否为数字
30
+ * @example
31
+ * isDigits('12') // true
32
+ * isDigits('1.2') // false
33
+ * isDigits('-12') // false
34
+ * isDigits('a12') // false
35
+ */
36
+ export function isDigits(s: string) {
37
+ return /^[0-9]+$/.test(s);
38
+ }
39
+
40
+ /**
41
+ * 数字字符串格式校验
42
+ * @param value 待验证值(字符串或数字)
43
+ * @param options 可选项
44
+ * @param options.negative 是否允许负数,默认 false
45
+ * @param options.decimal 小数位数,默认 2(0 表示必须整数)
46
+ * @param options.thousands 是否允许千分位逗号,默认 false
47
+ * @param options.leadZero 是否允许整数部分出现多余前导0 (默认false, 即禁止'007',但允许'0.50')
48
+ * @example
49
+ * isNumeric('123.45'); // true
50
+ * isNumeric('123.45', { decimal: 0 }); // false
51
+ * isNumeric('-1,234.5', { negative: true, thousands: true }); // true
52
+ * isNumeric('0123', { leadZero: true }); // true(现在允许)
53
+ * isNumeric('0123'); // false(默认禁止)
54
+ * isNumeric('0.50'); // true(始终允许)
55
+ * isNumeric('.5'); // false(整数部分不能省)
56
+ * isNumeric('123.'); // false(小数部分不能省)
57
+ */
58
+ export function isNumeric(
59
+ value: string | number,
60
+ options?: {
61
+ negative?: boolean;
62
+ decimal?: number;
63
+ thousands?: boolean;
64
+ leadZero?: boolean;
65
+ },
66
+ ): boolean {
67
+ const { negative = false, decimal = 2, thousands = false, leadZero = false } = options || {};
68
+
69
+ if (value === null || value === undefined || value === '') return false;
70
+ const str = String(value).trim();
71
+
72
+ const sign = negative && str.startsWith('-') ? '-' : '';
73
+ const body = sign ? str.slice(1) : str;
74
+
75
+ const thousandsPart = thousands ? '(?:[1-9]\\d{0,2}(,\\d{3})*|0)' : '(?:\\d+)';
76
+
77
+ const intPart = thousands ? thousandsPart : leadZero ? '(?:\\d+)' : '(?:0|[1-9]\\d*)';
78
+
79
+ const fracPart = decimal === 0 ? '' : `(\\.\\d{1,${decimal}})`;
80
+
81
+ const pattern = `^${intPart}${fracPart}$`;
82
+ const reg = new RegExp(pattern);
83
+ return reg.test(body);
84
+ }
85
+
86
+ /**
87
+ * 是否为中国大陆手机号(11 位,以 1 开头,第二位 3-9)。
88
+ * @param s 待校验的号码
89
+ * @returns 是否为合法手机号
90
+ * @example
91
+ * isMobilePhone('13800138000') // true
92
+ * isMobilePhone('12800138000') // false
93
+ */
94
+ export function isMobilePhone(s: string) {
95
+ const v = String(s ?? '').trim();
96
+ return /^1[3-9]\d{9}$/.test(v);
97
+ }
98
+
99
+ /**
100
+ * 是否为中国大陆座机号(区号-号码,可选分机)。
101
+ * 格式:`0AA-BBBBBBBB(-EXT)`,其中区号 2-3 位、号码 7-8 位、分机 1-6 位。
102
+ * @param s 待校验的号码
103
+ * @returns 是否为合法座机号
104
+ * @example
105
+ * isLandline('010-88888888') // true
106
+ * isLandline('0371-12345678-123') // true
107
+ */
108
+ export function isLandline(s: string) {
109
+ const v = String(s ?? '').trim();
110
+ return /^0\d{2,3}-?\d{7,8}(?:-\d{1,6})?$/.test(v);
111
+ }
112
+
113
+ /**
114
+ * 联系电话:是否为中国大陆“手机号或座机号”。
115
+ * @param s 待校验的号码
116
+ * @returns 是否为合法的手机号或座机号
117
+ * @example
118
+ * isPhone('13800138000') // true
119
+ * isPhone('010-88888888') // true
120
+ */
121
+ export function isPhone(s: string) {
122
+ return isMobilePhone(s) || isLandline(s);
123
+ }
124
+
125
+ /**
126
+ * 校验邮箱地址(基于 RFC 5322 的常用子集)。
127
+ * @param s 待校验的邮箱字符串
128
+ * @returns 是否为合法邮箱
129
+ * @example
130
+ * isEmail('user@example.com') // true
131
+ * isEmail('invalid@') // false
132
+ */
133
+ export function isEmail(s: string) {
134
+ const v = String(s ?? '').trim();
135
+ if (v === '') return false;
136
+ const emailRegex =
137
+ /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/;
138
+ return emailRegex.test(v);
139
+ }
140
+
141
+ /**
142
+ * 中文姓名(允许中间点 `·`),长度 2-20。
143
+ * @param s 姓名
144
+ * @returns 是否为合法中文姓名
145
+ * @example
146
+ * isChineseName('张三') // true
147
+ * isChineseName('阿·娜') // true
148
+ */
149
+ export function isChineseName(s: string) {
150
+ const v = String(s ?? '').trim();
151
+ return /^[\u4E00-\u9FA5·]{2,20}$/.test(v);
152
+ }
153
+
154
+ /**
155
+ * 身份证校验(支持中国大陆严格校验;台湾/香港/澳门做格式校验)。
156
+ * 规则:
157
+ * - 中国大陆(严格):18 位校验位 + 出生日期合法性;兼容 15 位旧号(日期校验);
158
+ * - 台湾(格式):`^[A-Z][12]\d{8}$`(首字母 + 性别位 1/2 + 8 位数字);不含校验位算法;
159
+ * - 香港(格式):`^[A-Z]{1,2}\d{6}\(?[0-9A]\)?$`(1-2 字母 + 6 位数字 + 校验位,可带括号);
160
+ * - 澳门(格式):常见为 `^[157]\d{6}\(?\d\)?$`(类别位 1/5/7 + 7 位数字 + 校验位,可带括号)。
161
+ * @param code 身份证号码
162
+ * @returns 是否为合法身份证号
163
+ * @example
164
+ * isIdentityCard('11010519491231002X') // true(大陆)
165
+ * isIdentityCard('A123456789') // true(台湾格式)
166
+ * isIdentityCard('A123456(3)') // true(香港格式)
167
+ * isIdentityCard('1234567(8)') // true(澳门格式)
168
+ */
169
+ export function isIdentityCard(code: string) {
170
+ const v = String(code ?? '').trim();
171
+ if (v === '') return false;
172
+
173
+ const isValidDate = (yyyymmdd: string) => {
174
+ const y = Number(yyyymmdd.slice(0, 4));
175
+ const m = Number(yyyymmdd.slice(4, 6));
176
+ const d = Number(yyyymmdd.slice(6, 8));
177
+ if (y < 1900 || y > 2100) return false;
178
+ const date = new Date(y, m - 1, d);
179
+ return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d;
180
+ };
181
+
182
+ // 18位校验
183
+ if (/^\d{17}[\dXx]$/.test(v)) {
184
+ const birth = v.slice(6, 14);
185
+ if (!isValidDate(birth)) return false;
186
+ const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
187
+ const checkMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
188
+ let sum = 0;
189
+ for (let i = 0; i < 17; i++) sum += Number(v[i]) * weights[i];
190
+ const mod = sum % 11;
191
+ const code18 = v[17].toUpperCase();
192
+ return checkMap[mod] === code18;
193
+ }
194
+
195
+ // 15位旧号:仅校验日期(第7~12位),不做校验位
196
+ if (/^\d{15}$/.test(v)) {
197
+ const birth = v.slice(6, 12);
198
+ const y = Number(`19${birth.slice(0, 2)}`);
199
+ const m = Number(birth.slice(2, 4));
200
+ const d = Number(birth.slice(4, 6));
201
+ const date = new Date(y, m - 1, d);
202
+ return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d;
203
+ }
204
+
205
+ // 台湾(格式校验)
206
+ if (/^[A-Za-z][12]\d{8}$/.test(v)) return true;
207
+
208
+ // 香港(格式校验):支持最后一位带括号与不带括号
209
+ if (/^[A-Za-z]{1,2}\d{6}\(?[0-9A]\)?$/.test(v)) return true;
210
+
211
+ // 澳门(格式校验):常见 1/5/7 开头 + 7位数字 + 校验位,可带括号
212
+ if (/^[157]\d{6}\(?\d\)?$/.test(v)) return true;
213
+
214
+ return false;
215
+ }
216
+
217
+ /**
218
+ * 护照号码校验(宽松通用格式,适合单输入框无法确认国家的场景)。
219
+ * 说明:各国护照格式差异较大,此函数提供常见模式的格式校验;不做校验位算法。
220
+ * 包含:
221
+ * - 中国护照常见:`E/G` 开头 + 8 位数字;`D/P/S` 开头 + 7 位数字;
222
+ * - 台湾护照常见:首字母 + 8 位数字;
223
+ * - 通用兜底:6-9 位的字母数字组合;移除输入中的空格与 `-` 后再校验。
224
+ * @param s 护照号码
225
+ * @returns 是否匹配常见护照格式
226
+ * @example
227
+ * isPassport('E12345678') // true
228
+ * isPassport('P1234567') // true
229
+ * isPassport('A12345678') // true(台湾常见)
230
+ * isPassport('AB-1234567') // true(移除分隔符后匹配)
231
+ */
232
+ export function isPassport(s: string) {
233
+ const t = String(s ?? '')
234
+ .replace(/[-\s]/g, '')
235
+ .trim();
236
+ if (t === '') return false;
237
+ if (/^[EG]\d{8}$/.test(t)) return true; // CN:E/G + 8 digits
238
+ if (/^[DPS]\d{7}$/.test(t)) return true; // CN:D/P/S + 7 digits
239
+ if (/^[A-Za-z]\d{8}$/.test(t)) return true; // TW:letter + 8 digits
240
+ if (/^[A-Za-z0-9]{6,9}$/.test(t)) return true; // 通用宽松兜底
241
+ return false;
242
+ }
243
+
244
+ /**
245
+ * 港澳通行证(回乡证)号码校验。
246
+ * 说明:常见为 `H/M` 开头 + 8~10 位数字;自动移除输入中的空格与 `-`。
247
+ * @param s 证件号
248
+ * @returns 是否为港澳通行证格式
249
+ * @example
250
+ * isHKMOPermit('H12345678') // true
251
+ * isHKMOPermit('M1234567890') // true
252
+ */
253
+ export function isHKMOPermit(s: string) {
254
+ const t = String(s ?? '')
255
+ .replace(/[-\s]/g, '')
256
+ .trim()
257
+ .toUpperCase();
258
+ return /^[HM]\d{8,10}$/.test(t);
259
+ }
260
+
261
+ /**
262
+ * 台湾居民来往大陆通行证(台胞证)号码校验。
263
+ * 说明:常见为 8 位纯数字;或首字母 + 8 位数字;部分场景存在 10 位数字。
264
+ * @param s 证件号
265
+ * @returns 是否为台胞证格式
266
+ * @example
267
+ * isTaiwanPermit('12345678') // true
268
+ * isTaiwanPermit('T12345678') // true
269
+ * isTaiwanPermit('1234567890') // true
270
+ */
271
+ export function isTaiwanPermit(s: string) {
272
+ const t = String(s ?? '')
273
+ .replace(/[-\s]/g, '')
274
+ .trim()
275
+ .toUpperCase();
276
+ if (/^\d{8}$/.test(t)) return true;
277
+ if (/^[A-Z]\d{8}$/.test(t)) return true;
278
+ if (/^\d{10}$/.test(t)) return true;
279
+ return false;
280
+ }
281
+
282
+ /**
283
+ * 军官证号码校验:字母数字组合,长度 7-18。
284
+ * @param s 证件号
285
+ * @returns 是否为军官证宽松格式
286
+ * @example
287
+ * isOfficerIdLoose('JX1234567') // true
288
+ */
289
+ export function isOfficerId(s: string) {
290
+ const t = String(s ?? '')
291
+ .replace(/[-\s]/g, '')
292
+ .trim()
293
+ .toUpperCase();
294
+ return /^[A-Z0-9]{7,18}$/.test(t);
295
+ }
296
+
297
+ /**
298
+ * 士兵证号码校验:字母数字组合,长度 7-18。
299
+ * @param s 证件号
300
+ * @returns 是否为士兵证格式
301
+ * @example
302
+ * isSoldierId('SB12345678') // true
303
+ */
304
+ export function isSoldierId(s: string) {
305
+ const t = String(s ?? '')
306
+ .replace(/[-\s]/g, '')
307
+ .trim()
308
+ .toUpperCase();
309
+ return /^[A-Z0-9]{7,18}$/.test(t);
310
+ }
311
+
312
+ /**
313
+ * 中国军证(军官证/士兵证)组合校验。
314
+ * @param s 证件号
315
+ * @returns 是否为军官证或士兵证格式
316
+ * @example
317
+ * isCnMilitaryId('JX1234567') // true
318
+ */
319
+ export function isMilitaryId(s: string) {
320
+ return isOfficerId(s) || isSoldierId(s);
321
+ }
322
+
323
+ /**
324
+ * 银行卡号校验(Luhn 校验)。
325
+ * 说明:移除空格与 `-` 后进行 Luhn 校验;长度通常为 12-19 位。
326
+ * @param s 银行卡号
327
+ * @returns 是否通过 Luhn 校验
328
+ * @example
329
+ * isBankCard('6222 0201 2345 6789') // true
330
+ */
331
+ export function isBankCard(s: string) {
332
+ const t = String(s ?? '')
333
+ .replace(/[-\s]/g, '')
334
+ .trim();
335
+ if (!/^\d{12,19}$/.test(t)) return false;
336
+ let sum = 0;
337
+ let shouldDouble = false;
338
+ for (let i = t.length - 1; i >= 0; i--) {
339
+ let digit = Number(t[i]);
340
+ if (shouldDouble) {
341
+ digit *= 2;
342
+ if (digit > 9) digit -= 9;
343
+ }
344
+ sum += digit;
345
+ shouldDouble = !shouldDouble;
346
+ }
347
+ return sum % 10 === 0;
348
+ }
349
+
350
+ /**
351
+ * 中国车牌号校验(含普通与新能源)。
352
+ * @param s 车牌号码
353
+ * @returns 是否为合法中国车牌
354
+ * @example
355
+ * isLicensePlate('京A12345') // true
356
+ * isLicensePlate('沪A12345D') // 新能源(末位 D/F)
357
+ * isLicensePlate('粤BDF12345') // 新能源(第三位 D/F)
358
+ */
359
+ export function isLicensePlate(s: string) {
360
+ const v = String(s ?? '')
361
+ .trim()
362
+ .toUpperCase();
363
+ const prov = '京沪津渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵青藏川宁琼粤';
364
+ const std = new RegExp(`^[${prov}][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$`);
365
+ const ne1 = new RegExp(`^[${prov}][A-HJ-NP-Z][DF][A-HJ-NP-Z0-9]{5}$`);
366
+ const ne2 = new RegExp(`^[${prov}][A-HJ-NP-Z][A-HJ-NP-Z0-9]{5}[DF]$`);
367
+ return std.test(v) || ne1.test(v) || ne2.test(v);
368
+ }
369
+
370
+ /**
371
+ * 校验统一社会信用代码(中国税号常用:18 位,含校验位)。
372
+ * 规则:
373
+ * - 字符集:数字与大写字母(不含 I/O/Z/S/V),即 `[0-9A-HJ-NPQRTUWXY]`;
374
+ * - 前 17 位参与加权求和,最后一位为校验码(取值 0-9 或 大写字母)。
375
+ * @param code 税号/统一社会信用代码
376
+ * @returns 是否为合法税号
377
+ * @example
378
+ * isTaxID('91350100M000100Y43') // true/false 取决于校验位
379
+ */
380
+ export function isTaxID(code: string) {
381
+ const v = String(code ?? '').trim();
382
+ if (!/^[0-9A-HJ-NPQRTUWXY]{18}$/.test(v)) return false;
383
+ const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY'; // 31 字符集
384
+ const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
385
+ const map: Record<string, number> = {};
386
+ for (let i = 0; i < charset.length; i++) map[charset[i]] = i;
387
+ let sum = 0;
388
+ for (let i = 0; i < 17; i++) {
389
+ sum += map[v[i]] * weights[i];
390
+ }
391
+ const logicCheck = (31 - (sum % 31)) % 31;
392
+ const expected = charset[logicCheck];
393
+ return v[17] === expected;
394
+ }
395
+
396
+ /**
397
+ * 判断字符串是否为合法 JSON 文本。
398
+ * 说明:传入字符串时尝试 `JSON.parse`;传入对象/数组则视为合法。
399
+ * @param input 待判定的值或字符串
400
+ * @returns 是否为合法 JSON
401
+ * @example
402
+ * isJSON('{"a":1}') // true
403
+ * isJSON('[1,2]') // true
404
+ * isJSON('abc') // false
405
+ */
406
+ export function isJSON(input: unknown) {
407
+ if (typeof input === 'string') {
408
+ const s = input.trim();
409
+ if (s === '') return false;
410
+ try {
411
+ JSON.parse(s);
412
+ return true;
413
+ } catch {
414
+ return false;
415
+ }
416
+ }
417
+ if (input !== null && typeof input === 'object') return true;
418
+ return false;
419
+ }
420
+
421
+ /**
422
+ * HEX 颜色值(支持 `#RGB`、`#RRGGBB`、`#RRGGBBAA`)。
423
+ * @param s 颜色字符串
424
+ * @returns 是否为合法 HEX 颜色
425
+ * @example
426
+ * isHexColor('#fff') // true
427
+ * isHexColor('#00ff00') // true
428
+ * isHexColor('#11223344') // true
429
+ */
430
+ export function isHexColor(s: string) {
431
+ const v = String(s ?? '').trim();
432
+ return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(v);
433
+ }
434
+
435
+ /**
436
+ * 校验 URL(要求含协议,支持 http/https/ftp)。
437
+ * @param s 待校验的地址
438
+ * @returns 是否为合法 URL
439
+ * @example
440
+ * isURL('https://example.com/path?a=1') // true
441
+ * isURL('example.com') // false(缺少协议)
442
+ */
443
+ export function isURL(s: string) {
444
+ const v = String(s ?? '').trim();
445
+ if (v === '') return false;
446
+ try {
447
+ const u = new URL(v);
448
+ return ['http:', 'https:', 'ftp:'].includes(u.protocol) && !!u.hostname;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * 判断是否为合法 IPv4 地址。
456
+ * @param s IP 字符串
457
+ * @returns 是否为合法 IPv4
458
+ * @example
459
+ * isIPv4('192.168.0.1') // true
460
+ * isIPv4('256.0.0.1') // false
461
+ */
462
+ function isIPv4(s: string) {
463
+ const v = String(s ?? '').trim();
464
+ if (v === '') return false;
465
+ const parts = v.split('.');
466
+ if (parts.length !== 4) return false;
467
+ for (const p of parts) {
468
+ if (!/^\d+$/.test(p)) return false;
469
+ if (p.length > 1 && p.startsWith('0')) return false;
470
+ const n = Number(p);
471
+ if (n < 0 || n > 255) return false;
472
+ }
473
+ return true;
474
+ }
475
+
476
+ /**
477
+ * 判断是否为合法 IPv6 地址(支持压缩表示与 IPv4 映射)。
478
+ * 规则:
479
+ * - 由 8 组 1~4 位十六进制数组成,允许一次 `::` 压缩;
480
+ * - 允许最后一组使用 IPv4 映射(如 `::ffff:192.168.0.1`)。
481
+ * @param s IP 字符串
482
+ * @returns 是否为合法 IPv6
483
+ * @example
484
+ * isIPv6('2001:0db8:85a3:0000:0000:8a2e:0370:7334') // true
485
+ * isIPv6('2001:db8::8a2e:370:7334') // true
486
+ * isIPv6('2001:::370:7334') // false
487
+ */
488
+ export function isIPv6(s: string): boolean {
489
+ const v = String(s ?? '').trim();
490
+ if (v === '') return false;
491
+
492
+ const lastColon = v.lastIndexOf(':');
493
+ if (lastColon !== -1 && v.includes('.')) {
494
+ const ipv6Part = v.slice(0, lastColon);
495
+ const ipv4Part = v.slice(lastColon + 1);
496
+ return isIPv6(ipv6Part) && isIPv4(ipv4Part);
497
+ }
498
+
499
+ const dblColonCount = (v.match(/::/g) || []).length;
500
+ if (dblColonCount > 1) return false;
501
+
502
+ const segments = v.split(':');
503
+ if (v.startsWith('::')) segments.shift();
504
+ if (v.endsWith('::')) segments.pop();
505
+ const segmentsFiltered = segments.filter((seg) => seg !== '');
506
+
507
+ if (dblColonCount === 0 && segmentsFiltered.length !== 8) return false;
508
+ if (dblColonCount === 1 && segmentsFiltered.length >= 1 && segmentsFiltered.length <= 7) {
509
+ // ok
510
+ } else if (dblColonCount === 1 && segments.length === 0) {
511
+ // :: 表示全部为 0
512
+ return true;
513
+ } else if (dblColonCount === 0 && segmentsFiltered.length === 8) {
514
+ // ok
515
+ } else {
516
+ return false;
517
+ }
518
+
519
+ // 每段 1~4 位十六进制
520
+ return segmentsFiltered.every(
521
+ (seg) => seg.length >= 1 && seg.length <= 4 && /^[0-9a-fA-F]{1,4}$/.test(seg),
522
+ );
523
+ }
524
+
525
+ /**
526
+ * 校验 IP(支持 IPv4 与 IPv6)。
527
+ * @param s IP 字符串
528
+ * @param version 指定版本:传 `4` 仅校验 IPv4,传 `6` 仅校验 IPv6;缺省同时校验两者
529
+ * @returns 是否为合法 IP
530
+ * @example
531
+ * isIP('127.0.0.1') // true
532
+ * isIP('::1') // true
533
+ * isIP('127.0.0.1', 6) // false
534
+ */
535
+ export function isIP(s: string, version?: 4 | 6 | '4' | '6') {
536
+ if (version === 4 || version === '4') return isIPv4(s);
537
+ if (version === 6 || version === '6') return isIPv6(s);
538
+ return isIPv4(s) || isIPv6(s);
539
+ }
540
+
541
+ /**
542
+ * 校验 CIDR IP 段(支持 IPv4/IPv6),形如 `IP/前缀长度`。
543
+ * @param s CIDR 字符串,如 `192.168.0.0/24`、`2001:db8::/32`
544
+ * @returns 是否为合法 CIDR
545
+ * @example
546
+ * isIPRange('10.0.0.0/8') // true
547
+ * isIPRange('2001:db8::/129') // false
548
+ */
549
+ export function isIPRange(s: string) {
550
+ const v = String(s ?? '').trim();
551
+ if (v === '') return false;
552
+ const parts = v.split('/');
553
+ if (parts.length !== 2) return false;
554
+ const [ip, prefixStr] = parts;
555
+ if (!/^\d+$/.test(prefixStr)) return false;
556
+ const prefix = Number(prefixStr);
557
+ if (ip.includes(':')) {
558
+ if (!isIPv6(ip)) return false;
559
+ return prefix >= 0 && prefix <= 128;
560
+ }
561
+ if (!isIPv4(ip)) return false;
562
+ return prefix >= 0 && prefix <= 32;
563
+ }
564
+
565
+ /**
566
+ * 端口号校验(0 ~ 65535,整数)。
567
+ * @param s 端口(字符串或数字)
568
+ * @returns 是否为合法端口范围
569
+ * @example
570
+ * isPortNumber(80) // true
571
+ * isPortNumber('65535') // true
572
+ * isPortNumber(70000) // false
573
+ */
574
+ export function isPortNumber(s: string | number) {
575
+ const v = typeof s === 'number' ? s : Number(String(s ?? '').trim());
576
+ return Number.isInteger(v) && v >= 0 && v <= 65535;
577
+ }
578
+
579
+ /**
580
+ * 纬度校验(-90 ~ 90)。
581
+ * @param s 纬度值(字符串或数字)
582
+ * @returns 是否为合法纬度
583
+ * @example
584
+ * isLatitude('31.2304') // true
585
+ */
586
+ export function isLatitude(s: string | number) {
587
+ const v = typeof s === 'number' ? s : Number(String(s ?? '').trim());
588
+ return Number.isFinite(v) && v >= -90 && v <= 90;
589
+ }
590
+
591
+ /**
592
+ * 经度校验(-180 ~ 180)。
593
+ * @param s 经度值(字符串或数字)
594
+ * @returns 是否为合法经度
595
+ * @example
596
+ * isLongitude('121.4737') // true
597
+ */
598
+ export function isLongitude(s: string | number) {
599
+ const v = typeof s === 'number' ? s : Number(String(s ?? '').trim());
600
+ return Number.isFinite(v) && v >= -180 && v <= 180;
601
+ }
package/index.cjs DELETED
@@ -1,2 +0,0 @@
1
- // CJS entry point for TypeScript 4.x compatibility
2
- module.exports = require('./dist/index.cjs');
package/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export * from './dist/index.d.ts';
package/index.js DELETED
@@ -1 +0,0 @@
1
- export * from './dist/index.js';