如何将含Unicode普通分数的字符串转换为CGFloat
问题描述
我有一个可能包含任意内容的字符串,例如"1"、"1½"或"one."。如果该字符串是数字,我希望将其转换为CGFloat。对于"1"这类简单情况,用NumberFormatter就能实现:
if let newFloat = NumberFormatter().number(from: string) { let float = CGFloat(truncating: newFloat) print(float) }
但这段代码处理不了"½"这类普通分数。我写了一个String扩展来把普通分数替换成小数,但有两个问题:一是实现不够优雅,二是没法正确处理"1⅓"这类带整数的分数,会输出"10.333"。以下是我的扩展代码,求更优实现方式:
extension String { func replaceVulgarFractions() -> String { var modifiedText = self let vulgarFractionRegex = "[½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞]" while let matchRange = modifiedText.range(of: vulgarFractionRegex, options: .regularExpression) { let matchedFraction = String(modifiedText[matchRange]) if let decimalValue = matchedFraction.vulgarFractionToDecimal() { modifiedText = modifiedText.replacingCharacters(in: matchRange, with: String(format: "%.3f", decimalValue)) } } return modifiedText } func vulgarFractionToDecimal() -> Double? { switch self { case "½": return 0.5 case "⅓": return 1.0 / 3.0 case "⅔": return 2.0 / 3.0 case "¼": return 0.25 case "¾": return 0.75 case "⅕": return 0.2 case "⅖": return 0.4 case "⅗": return 0.6 case "⅘": return 0.8 case "⅙": return 1.0 / 6.0 case "⅚": return 5.0 / 6.0 case "⅐": return 1.0 / 7.0 case "⅛": return 1.0 / 8.0 case "⅜": return 3.0 / 8.0 case "⅝": return 5.0 / 8.0 case "⅞": return 7.0 / 8.0 default: return nil } } }
优化实现方案
核心思路
- 优先识别
整数+分数的组合(如1⅓),拆分后计算总和再替换 - 单独的分数直接映射为小数
- 用字典替代冗长的switch分支,简化维护
- 一次性批量替换所有匹配项,避免循环处理的低效
优化后的代码
extension String { // 普通分数与小数的映射字典,新增分数只需添加键值对 private static let vulgarFractionMap: [Character: Double] = [ "½": 0.5, "⅓": 1/3, "⅔": 2/3, "¼": 0.25, "¾": 0.75, "⅕": 0.2, "⅖": 0.4, "⅗": 0.6, "⅘": 0.8, "⅙": 1/6, "⅚": 5/6, "⅐": 1/7, "⅛": 1/8, "⅜": 3/8, "⅝": 5/8, "⅞": 7/8 ] func convertVulgarFractionsToDecimal() -> String { var result = self // 处理带整数的分数:匹配「数字+分数」格式 let integerFractionRegex = "(\\d+)([½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞])" result = result.replacingOccurrences( of: integerFractionRegex, with: { match in guard let integerPart = Double(match.1), let fractionValue = String.vulgarFractionMap[Character(match.2)] else { return match.0 // 匹配失败则返回原内容 } let total = integerPart + fractionValue return String(format: "%.6f", total).trimmingTrailingZeros() }, options: .regularExpression ) // 处理单独的分数 let singleFractionRegex = "([½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞])" result = result.replacingOccurrences( of: singleFractionRegex, with: { match in guard let fractionValue = String.vulgarFractionMap[Character(match.1)] else { return match.0 } return String(format: "%.6f", fractionValue).trimmingTrailingZeros() }, options: .regularExpression ) return result } // 辅助方法:移除小数末尾多余的0和小数点 private func trimmingTrailingZeros() -> String { let trimmed = self.replacingOccurrences(of: "\\.?0+$", with: "", options: .regularExpression) return trimmed.isEmpty ? "0" : trimmed } } // 使用示例 let testStrings = ["1", "½", "1⅓", "2¾", "⅝", "one."] for str in testStrings { let converted = str.convertVulgarFractionsToDecimal() if let number = NumberFormatter().number(from: converted), let cgFloat = CGFloat(exactly: number) { print("\(str) -> \(cgFloat)") } else { print("\(str) 无法转换为CGFloat") } }
优化点说明
- 字典映射替代switch:用
vulgarFractionMap存储分数与对应值,代码更简洁,新增分数只需添加键值对即可 - 正则匹配整数+分数:通过正则
(\\d+)([分数字符])精准匹配带整数的分数,计算总和后替换,彻底解决1⅓变成10.333的问题 - 批量替换提升效率:利用
replacingOccurrences的闭包版本一次性处理所有匹配项,无需循环遍历字符串 - 美化小数格式:
trimmingTrailingZeros方法自动移除末尾多余的0和小数点,输出更简洁的小数格式
内容的提问来源于stack exchange,提问作者Moses Harding
相关产品推荐
相关产品推荐

