/** * 将比例化简为最简整数比 * * 比如输入 800:200,输出 "4:1",输入 1920:1080,输出 "16:9" * * @param a 比例前项 * @param b 比例后项 * @param options 配置选项 * @returns 格式为 "前项:后项" 的字符串 */ export function simplifyRatio( a: number, b: number, options?: { /** * 小数位数控制: * - undefined/true: 返回最简整数比(默认) * - 数字: 允许小数,保留指定位数以提高可读性 * - false: 同 undefined */ round?: number | boolean /** * 屏幕比例优化: * - true: 优先使用标准屏幕比例格式(如 16:9, 16:10, 4:3 等) * - false/undefined: 使用标准最简比例 */ screenRatio?: boolean } ): string { if (b == 0 || a == 0 || isNaN(a) || isNaN(b)) { return "" } // 先计算最简整数比 const gcd = getGCD(Math.abs(a), Math.abs(b)) const intA = a / gcd const intB = b / gcd // 屏幕比例优化 if (options?.screenRatio) { const screenRatio = getOptimizedScreenRatio(a, b) if (screenRatio) return screenRatio } // 默认返回整数比 if (!options?.round || options.round === true || options.round === 0) { return `${intA}:${intB}` } // 尝试寻找更可读的小数比例 if (typeof options.round === "number") { const decimals = Math.max(0, Math.floor(options.round)) // 如果整数比已经足够简洁(两个数都 ≤ 20),直接返回整数比 if (intA <= 20 && intB <= 20) { return `${intA}:${intB}` } // 尝试不同的前项值,找到可读性最好的表示 let bestRatio = `${intA}:${intB}` let bestScore = getRatioReadabilityScore(intA, intB) // 尝试前项为 1 到 20 的情况 for (let targetA = 1; targetA <= 20; targetA++) { const targetB = (intB * targetA) / intA // 四舍五入到指定小数位 const roundedB = Number(targetB.toFixed(decimals)) // 检查误差是否可接受(<2%) const error = Math.abs(targetB - roundedB) / targetB if (error > 0.02) continue // 评估可读性 const score = getRatioReadabilityScore(targetA, roundedB) if (score < bestScore) { bestScore = score const bStr = decimals > 0 ? roundedB.toFixed(decimals) : String(roundedB) bestRatio = `${targetA}:${bStr}` } } return bestRatio } return `${intA}:${intB}` } /** * 获取优化的屏幕比例表示 */ function getOptimizedScreenRatio(a: number, b: number): string | null { const ratio = a / b // 常见的屏幕比例标准(比例值, 显示格式, 推荐使用的数值范围) const screenRatios: Array<[number, string, number]> = [ [16 / 9, "16:9", 16], // 1.778 - 最常见的宽屏 [16 / 10, "16:10", 16], // 1.6 - 常见笔记本比例 [4 / 3, "4:3", 4], // 1.333 - 传统屏幕比例 [3 / 2, "3:2", 3], // 1.5 - Surface 等设备 [21 / 9, "21:9", 21], // 2.333 - 超宽屏 [32 / 9, "32:9", 32], // 3.556 - 超超宽屏 [5 / 4, "5:4", 5], // 1.25 - 老式显示器 ] // 容差范围:1% const tolerance = 0.01 for (const [standardRatio, format, preferredA] of screenRatios) { const diff = Math.abs(ratio - standardRatio) / standardRatio if (diff <= tolerance) { // 检查输入值是否适合使用这种标准格式 // 只有当输入的前项值相对较大(接近屏幕分辨率的范围)时才使用标准格式 if (a >= 100 || b >= 100) { // 屏幕分辨率通常是三位数以上 return format } } } return null } function getRatioReadabilityScore(a: number, b: number): number { // 数值大小惩罚(偏好 1-20 范围内的数字) const sizeScore = (a > 20 ? a - 20 : 0) + (b > 20 ? b - 20 : 0) // 小数惩罚(偏好避免过小的小数) const decimalScore = (b < 1 ? 20 : 0) + (a < 1 ? 20 : 0) // 数值范围惩罚(偏好较小的数字) const rangeScore = Math.max(a, b) return sizeScore + decimalScore + rangeScore * 0.1 } /** * 计算两个数的最大公约数,支持小数 */ const getGCD = (a: number, b: number): number => { // 处理小数:转换为整数后计算 GCD let factor = 1 while (!Number.isInteger(a * factor) || !Number.isInteger(b * factor)) { factor *= 10 // 防止无限循环 if (factor > 1e10) break } const intA = Math.round(Math.abs(a * factor)) const intB = Math.round(Math.abs(b * factor)) const gcd = gcdIntegers(intA, intB) return gcd / factor } /** * 计算两个整数的最大公约数(辗转相除法) */ const gcdIntegers = (a: number, b: number): number => { return b === 0 ? a : gcdIntegers(b, a % b) }