stringUtils.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import compose from 'ramda/es/compose';
  2. import filter from 'ramda/es/filter';
  3. import isNil from 'ramda/es/isNil';
  4. import map from 'ramda/es/map';
  5. import split from 'ramda/es/split';
  6. import trim from 'ramda/es/trim';
  7. /**
  8. * 移除字符串首尾的指定字符(仅各移除一个,若首/尾等于 char)。
  9. * 支持柯里化:仅传 char 时返回 (str) => string。
  10. * @param char 要移除的单个字符
  11. * @param str 原字符串;省略时返回柯里化函数
  12. * @returns 处理后的字符串,或柯里化后的函数
  13. */
  14. function trimChar(char: string, str: string): string;
  15. function trimChar(char: string): (str: string) => string;
  16. function trimChar(char: string, str?: string) {
  17. if (isNil(str)) {
  18. return (str1: string) => {
  19. let result = str1;
  20. if (result.charAt(0) === char) {
  21. result = result.slice(1);
  22. }
  23. if (result.charAt(result.length - 1) === char) {
  24. result = result.slice(0, -1);
  25. }
  26. return result;
  27. };
  28. } else {
  29. let result = str;
  30. if (result.charAt(0) === char) {
  31. result = result.slice(1);
  32. }
  33. if (result.charAt(result.length - 1) === char) {
  34. result = result.slice(0, -1);
  35. }
  36. return result;
  37. }
  38. }
  39. export { trimChar };
  40. /**
  41. * 将字符串的首字母转为小写,其余不变;空字符串原样返回。
  42. * @param str 原字符串
  43. * @returns 首字母小写后的字符串
  44. */
  45. export function toLowerCaseFirstLetter(str: string) {
  46. if (!str) return str;
  47. return str.charAt(0).toLowerCase() + str.slice(1);
  48. }
  49. /**
  50. * 将字符串的首字母转为大写,其余不变;空字符串原样返回。
  51. * @param str 原字符串
  52. * @returns 首字母大写后的字符串
  53. */
  54. export function toUpperCaseFirstLetter(str: string) {
  55. if (!str) return str;
  56. return str.charAt(0).toUpperCase() + str.slice(1);
  57. }
  58. /**
  59. * 账号脱敏:前二后四保留,中间用 '**' 代替;长度 ≤8 时仅保留后四位并前缀 '**'。空串返回 ''。
  60. * @param str 原始账号字符串
  61. * @returns 脱敏后的字符串,如 'us**com.'、'**5678'
  62. */
  63. export function maskAccount(str: string): string {
  64. if (!str) return '';
  65. const len = str.length;
  66. if (len <= 8) return '**' + str.slice(-4);
  67. return str.slice(0, 2) + '**' + str.slice(-4);
  68. }
  69. /**
  70. * 判断字符串是否为常见邮箱格式(本地部分 + @ + 域名 + 至少两位 TLD)。
  71. * @param str 待检测字符串
  72. * @returns 符合邮箱格式返回 true,否则 false
  73. */
  74. export function isEmail(str: string): boolean {
  75. const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  76. return regex.test(str);
  77. }
  78. /**
  79. * 判断字符串是否为 Gmail 邮箱([email protected])。
  80. * @param str 待检测字符串
  81. * @returns 为 Gmail 格式返回 true,否则 false
  82. */
  83. export function isGmail(str: string): boolean {
  84. const regex = /^[a-zA-Z0-9._%+-]+@gmail([.])com$/;
  85. return regex.test(str);
  86. }
  87. /**
  88. * 判断字符串是否为合法 IPv4 地址(四段、每段 0–255)。
  89. * @param str 待检测字符串
  90. * @returns 合法 IPv4 返回 true,否则 false
  91. */
  92. export function isIp(str: string): boolean {
  93. const octet = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])';
  94. const regex = new RegExp(`^${octet}\\.${octet}\\.${octet}\\.${octet}$`);
  95. return regex.test(str);
  96. }
  97. /**
  98. * 生成具备加密强度的唯一标识(基于 crypto.getRandomValues + 时间戳,base36)。
  99. * 同一时刻多次调用不重复、不可预测,但较慢(依赖底层加密 API)。
  100. * @returns 唯一字符串,适合用作防篡改或安全场景的 sign
  101. */
  102. export function getCryptoUniqueSign() {
  103. const v = Array.from(crypto.getRandomValues(new Uint32Array(3)))
  104. .map((num) => num.toString().padStart(10, '0'))
  105. .join('');
  106. return BigInt(`${Date.now()}${v}`).toString(36);
  107. }
  108. /**
  109. * 生成唯一标识(基于 Math.random + 时间戳,base36)。同一时刻多次调用不重复,可预测、效率较高,不适用于安全场景。
  110. * @returns 唯一字符串,适合用作非安全场景的 ID 或 sign
  111. */
  112. export function getUniqueSign() {
  113. const v = [Math.floor(Math.random() * 2 ** 32), Math.floor(Math.random() * 2 ** 32)]
  114. .map((num) => num.toString().padStart(10, '0'))
  115. .join('');
  116. return BigInt(`${Date.now()}${v}`).toString(36);
  117. }
  118. /**
  119. * 将 IPv4 字符串转为 32 位无符号整数,语义与 MySQL INET_ATON 一致。非法或空串返回 0。
  120. * @param ip 点分十进制 IPv4 字符串
  121. * @returns 无符号整数,或 0(格式错误/空)
  122. */
  123. export function ipToNum(ip: string): number {
  124. if (!ip) return 0;
  125. const parts = ip.split('.');
  126. if (parts.length !== 4) return 0;
  127. const num0 = parseInt(parts[0], 10);
  128. const num1 = parseInt(parts[1], 10);
  129. const num2 = parseInt(parts[2], 10);
  130. const num3 = parseInt(parts[3], 10);
  131. if (
  132. isNaN(num0) ||
  133. num0 < 0 ||
  134. num0 > 255 ||
  135. isNaN(num1) ||
  136. num1 < 0 ||
  137. num1 > 255 ||
  138. isNaN(num2) ||
  139. num2 < 0 ||
  140. num2 > 255 ||
  141. isNaN(num3) ||
  142. num3 < 0 ||
  143. num3 > 255
  144. ) {
  145. return 0;
  146. }
  147. return ((num0 << 24) | (num1 << 16) | (num2 << 8) | num3) >>> 0;
  148. }
  149. /**
  150. * 将 32 位无符号整数转为 IPv4 点分十进制字符串,语义与 MySQL INET_NTOA 一致。超出 [0, 0xffffffff] 返回 ''。
  151. * @param num 无符号整数
  152. * @returns 点分十进制 IP 字符串,或 ''(越界)
  153. */
  154. export function numToIp(num: number): string {
  155. if (num < 0 || num > 0xffffffff) {
  156. return '';
  157. }
  158. const byte3 = (num >>> 24) & 0xff;
  159. const byte2 = (num >>> 16) & 0xff;
  160. const byte1 = (num >>> 8) & 0xff;
  161. const byte0 = num & 0xff;
  162. return `${byte3}.${byte2}.${byte1}.${byte0}`;
  163. }
  164. /**
  165. * 将字符串按逗号、空白(含换行)拆分为非空字符串数组;先 trim 再按 /[,\s]+/ 拆分,并对每项 trim、过滤空串。
  166. * @param str 原字符串(由 compose 管道传入,调用方式 toStrArray(str))
  167. * @returns 非空字符串数组
  168. */
  169. export const toStrArray = compose(
  170. filter((v) => v !== ''),
  171. map((v) => trim(v)),
  172. split(/[,\s]+/),
  173. trimChar(','),
  174. trim
  175. );
  176. /**
  177. * 根据字符组成检测文本主要语言:中文(含 CJK 等)与英文(拉丁字母)数量比较,空或仅空白返回 'unknown'。
  178. * @param text 待检测文本
  179. * @returns 'zh-CN' | 'en' | 'unknown'
  180. */
  181. export function detectLanguage(text: string): 'zh-CN' | 'en' | 'unknown' {
  182. if (!text || !text.trim()) {
  183. return 'unknown';
  184. }
  185. const trimmedText = text.trim();
  186. const chineseRegex =
  187. /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff\uff00-\uffef]/;
  188. const englishRegex = /[a-zA-Z]/;
  189. const hasChineseChars = chineseRegex.test(trimmedText);
  190. const hasEnglishChars = englishRegex.test(trimmedText);
  191. const chineseMatches = trimmedText.match(/[\u4e00-\u9fff]/g) || [];
  192. const englishMatches = trimmedText.match(/[a-zA-Z]/g) || [];
  193. const chineseCount = chineseMatches.length;
  194. const englishCount = englishMatches.length;
  195. if (hasChineseChars && (!hasEnglishChars || chineseCount > englishCount)) {
  196. return 'zh-CN';
  197. }
  198. if (hasEnglishChars && (!hasChineseChars || englishCount > chineseCount)) {
  199. return 'en';
  200. }
  201. return 'unknown';
  202. }
  203. /**
  204. * 根据标题文本检测语言并返回源语言代码;与 TranslateSourceLang 的 value 对应,未知时默认 'zh-CN'。
  205. * @param title 标题文本
  206. * @returns 'zh-CN' | 'en'(检测为英文时返回 'en',其余返回 'zh-CN')
  207. */
  208. export function getSourceLanguageFromTitle(title: string): string {
  209. const detectedLang = detectLanguage(title);
  210. switch (detectedLang) {
  211. case 'zh-CN':
  212. return 'zh-CN';
  213. case 'en':
  214. return 'en';
  215. default:
  216. return 'zh-CN';
  217. }
  218. }