/** * @description Convert a camelCase string into kebab-case. * * @example * ``` * // Expect: "hello-world" * const example1 = stringCamelCaseToKebabCase("helloWorld") * // Expect: "ab2-cd" * const example2 = stringCamelCaseToKebabCase("ab2Cd") * ``` */ export const stringCamelCaseToKebabCase = (camelCase: string): string => { return camelCase.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase() } /** * @description Convert a kebab-case string into camelCase. * * @example * ``` * // Expect: "helloWorld" * const example1 = stringKebabCaseToCamelCase("hello-world") * // Expect: "ab2Cd" * const example2 = stringKebabCaseToCamelCase("ab2-cd") * ``` */ export const stringKebabCaseToCamelCase = (kebabCase: string): string => { return kebabCase.replaceAll(/-./g, (x) => x[1]!.toUpperCase()) } /** * @description Get a greeting based on the current local hour. * * @example * ``` * // Expect: "你好" | "早上好" | "晚上好" | ... * const example1 = stringHelloWord() * ``` */ export const stringHelloWord = (): string => { const currentHour = new Date().getHours() if (currentHour < 5) { return "凌晨好" } if (currentHour < 8) { return "早上好" } if (currentHour < 12) { return "上午好" } if (currentHour < 14) { return "中午好" } if (currentHour < 17) { return "下午好" } if (currentHour < 19) { return "傍晚好" } if (currentHour < 24) { return "晚上好" } return "你好" } /** * @description Calculate the total unit length of a string. * * @example * ``` * // Expect: 1.5 * const example1 = stringCalculateUnits("a中") * // Expect: 2 * const example2 = stringCalculateUnits("中文") * ``` */ export const stringCalculateUnits = (text: string): number => { let units = 0 for (const char of text) { if (/[\u0000-\u007F]/.test(char)) { // Half-width character (e.g., ASCII) units = units + 0.5 } else { // Full-width character (e.g., CJK) units = units + 1 } } return units } /** * @description Truncate a string by the unit limit and keep whole characters. * * @example * ``` * // Expect: "a中" * const example1 = stringTruncateByUnits("a中文", 1.5) * // Expect: "abc" * const example2 = stringTruncateByUnits("abc", 2) * ``` */ export const stringTruncateByUnits = (text: string, maxUnits: number): string => { let truncated = "" let units = 0 for (const char of text) { const charUnits = /[\u0000-\u007F]/.test(char) ? 0.5 : 1 if (units + charUnits > maxUnits) { break } truncated = truncated + char units = units + charUnits } return truncated } /** * @description Slice a string by unit indices. * * @example * ``` * // Expect: "a中" * const example1 = stringSliceByUnits("a中文", 0, 1.5) * // Expect: "文" * const example2 = stringSliceByUnits("中文ab", 1, 2) * ``` */ export const stringSliceByUnits = (text: string, start: number, end: number): string => { let units = 0 let result = "" for (const char of text) { const charUnits = /[\u0000-\u007F]/.test(char) ? 0.5 : 1 if (units >= end) { break } if (units >= start) { result = result + char } units = units + charUnits } return result } export interface StringSplitOptions { input: string chunkSize?: number | undefined chunkOverlap?: number | undefined } /** * @description Split a long string into fixed-size chunks with optional overlap. * * @example * ``` * // Expect: ["hello", "lo wo", "world"] * const example1 = stringSplit({ input: "hello world", chunkSize: 5, chunkOverlap: 2 }) * ``` */ export const stringSplit = (options: StringSplitOptions): string[] => { const { input, chunkSize = 1_500, chunkOverlap: overlap = 20 } = options const safeChunkSize = Math.max(1, Math.trunc(chunkSize)) const safeOverlap = Math.max(0, Math.min(Math.trunc(overlap), safeChunkSize - 1)) // 去除换行符和其他特殊符号 const sanitizedInput = input.replaceAll(/[\t\n\r]/g, " ") // 分割结果数组 const result: string[] = [] // 当前处理位置 let currentPosition = 0 // 循环处理整个输入字符串 while (currentPosition < sanitizedInput.length) { // 计算当前片段的结束位置 const endPosition = Math.min(currentPosition + safeChunkSize, sanitizedInput.length) // 获取当前片段 const segment = sanitizedInput.slice(currentPosition, endPosition) // 将当前片段添加到结果数组中 result.push(segment) // 如果已经到达文本末尾,退出循环 if (endPosition >= sanitizedInput.length) { break } // 更新处理位置,有重叠部分 currentPosition = endPosition - safeOverlap } return result } /** * @description Split a string into chunks while trying to keep natural line breaks. * * @example * ``` * // Expect: ["a\\n", "b\\n", "c"] * const example1 = stringSmartSplit("a\nb\nc", 2) * ``` */ export const stringSmartSplit = (text: string, maxLength: number): string[] => { let _text = text if (_text.length <= maxLength) { return [_text] } const conservativeMaxLength = maxLength * 0.9 const threshold = maxLength * 0.2 const texts: string[] = [] while (_text.length > conservativeMaxLength) { texts.push(_text.slice(0, conservativeMaxLength)) _text = _text.slice(conservativeMaxLength) } if (_text.length > threshold) { texts.push(_text) } else { texts[texts.length - 1] = texts.at(-1)! + _text } if (texts.length === 1) { return texts } for (let i = 1; i < texts.length; i = i + 1) { const previousText = texts[i - 1]! const currentText = texts[i]! const lastIndexOfNewLineOfPreviousText = previousText.lastIndexOf("\n") const firstIndexOfNewLineOfCurrentText = currentText.indexOf("\n") const endOfPreviousText = previousText.slice(lastIndexOfNewLineOfPreviousText + 1) const startOfCurrentText = currentText.slice(0, firstIndexOfNewLineOfCurrentText) if (endOfPreviousText.length >= startOfCurrentText.length) { const newPreviousText = previousText + startOfCurrentText const newCurrentText = currentText.slice(firstIndexOfNewLineOfCurrentText + 1) texts[i - 1] = newPreviousText texts[i] = newCurrentText } else { const newPreviousText = previousText.slice(0, lastIndexOfNewLineOfPreviousText) const newCurrentText = endOfPreviousText + currentText texts[i - 1] = newPreviousText texts[i] = newCurrentText } } const trimmedTexts = texts.map((text) => text.trim()) return trimmedTexts } /** * @description Truncate a string to a maximum length with an ellipsis. * * @example * ``` * // Expect: "hello..." * const example1 = stringTruncate("hello world", 5) * // Expect: "hi" * const example2 = stringTruncate("hi", 5) * ``` */ export const stringTruncate = (str: string, n: number): string => { if (str.length <= n) { return str } else { return `${str.slice(0, n)}...` } }