使用 Soulver Core 进行字符串解析

一种声明式和类型安全的方法,用于从字符串中解析数据

SoulverCore 为您提供从 Swift 字符串中进行人性化、类型安全且高性能的数据解析。

指定您想从字符串中解析的类型。如果它们存在,您将获得可以直接使用的数据基本类型(而不是字符串!)。

这种数据解析方法允许您忽略

  1. 您需要的数据在文本中格式化的具体方式
  2. 随机单词(或其他数据点),围绕着您需要的数据

示例

让我们看几个例子

let (testCount, failureCount, timeTaken) = "Executed 4 tests, with 1 failure in 0.009 seconds".find(.number, .number, .time)!

testCount // 4
failureCount // 1
timeTaken // 0.009 seconds

let (date, temperature, humidity) = "On August 23, 2022 the temperature in Chicago was 68.3 ºF (with a humidity of 74%)".find(.date, .temperature, .percentage)!

date // August 23, 2022
temperature // 68.3 ºF
humidity // 74%

let (earnings, fileSize, url) = "Total Earnings From PDF: $12.2k (3.25 MB, at https://lifeadvice.co.uk/pdfs/download?id=guide)".find(.currency, .fileSize, .url)!

earnings // 12,200 USD
fileSize // 3.25 MB
url // https://lifeadvice.co.uk/pdfs/download?id=guide

注意:返回的数据点不是字符串。它们是原生 Swift 数据类型(作为元组中的元素提供),您可以立即对其执行操作

let numbers = "100 + 20".find(.number, .number)!
let sum = numbers.0 + numbers.1 // 120

单次调用最多可以请求 6 个数据点。Variadic 泛型计划在 Swift 6 中推出,因此我们将来会支持更多。

高阶数据提取的魅力

观察此处使用的高阶概念的魅力:数字有多种格式(1,000、30k、.456),但简单的“.number”查询“匹配”所有这些格式。.date 也“匹配”常用日期格式的日期。

对于区域设置在数据格式中起作用的情况,您可以在 find 方法中指定区域设置(否则将使用当前系统区域设置)

let europeanNumber = "€1.333,24".find(.currency, locale: Locale(identifier: "en_DE"))
let americanDate = "05/30/21".find(.date, locale: Locale(identifier: "en_US")) // month/day/year

在可能的情况下,会返回标准 Swift 基本类型(URL、Date、Decimal 等)。如果 Swift 基本类型无法完全捕获字符串中存在的数据,则会返回 SoulverCore 值类型,其中包含相关数据的属性。

支持的数据类型

符号 匹配示例 返回类型
.number 123.45, 10k, -.3, 3,000, 50_000 Decimal
.binaryNumber 0b1011010 UInt
.hexNumber 0x31FE28 UInt
.boolean 'true' 或 'false' Bool
.percentage 10%, 230.99% Decimal
.date March 12, 2004, 21/04/77, July the 4th, 等等 Date
.unixTimestamp 1661259854 TimeInterval
.place Paris, Tokyo, Bali, Israel SoulverCore.Place
.airport SFO, LAX, SYD SoulverCore.Place
.timezone AEST, GMT, EST SoulverCore.Place
.currencyCode USD, EUR, DOGE String
.currency $10.00, AU$30k, 350 JPY SoulverCore.UnitExpression
.time 10 s, 3 min, 4 weeks SoulverCore.UnitExpression
.distance 10 km, 3 miles, 4 cm SoulverCore.UnitExpression
.temperature 25 °C, 77 °F, 10C, 5 F SoulverCore.UnitExpression
.weight 10kg, 45 lb SoulverCore.UnitExpression
.area 30 m2, 40 in2 SoulverCore.UnitExpression
.speed 30 mph SoulverCore.UnitExpression
.volume 3 litres, 4 cups, 10 fl oz SoulverCore.UnitExpression
.timespan 3 hours 12 minutes SoulverCore.Timespan
.laptime 01:30:22.490 (hh:mm:ss.ms) SoulverCore.Laptime
.timecode 03:10:21:16 (hh:mm:ss:frames) SoulverCore.Frametime
.pitch A4, Bb7, C#9 SoulverCore.Pitch
.url https://soulver.app URL
.emailAddress bob@hotmail.com String
.hashTag #this_is_a_tag String
.whitespace 所有空白字符(包括制表符)都将折叠为单个空白标记 String

开始使用

在字符串中查找数据

正如我们在上面看到的,在字符串中查找数据点就像请求它一样简单

let percent = "Results of likeness test: 83% match".find(.percentage)
// percent is the decimal 0.83

提取多个数据点也同样容易。将返回一个元组,其中包含正确数量的参数和数据类型

let payrollEntry = "CREDIT			03/02/2022			Payroll from employer				$200.23" // this string has inconsistent whitespace between entities, but this isn't a problem for us
let (date, currency) = payrollEntry.find(.date, .currency)!
date // Either February 3, or March 2, depending on your system locale
currency // UnitExpression object (use .value to get the decimalValue, and .unit.identifier to get the currency code - USD)

从字符串数组中提取数据点

我们还可以对字符串数组调用带有单个数据类型的 find,并返回与匹配项对应的数组数据类型

let amounts = ["Zac spent $50", "Molly spent US$81.9 (with her 10% discount)", "Jude spent $43.90 USD"].find(.currency)

let totalAmount = amounts.reduce(0.0) {
    $0 + $1.value
}

// totalAmount is $175.80

转换字符串中的数据

假设我们想标准化上一个示例中字符串中的空格

let standardized = "CREDIT			03/02/2022			Payroll from employer				$200.23".replacingAll(.whitespace) { whitespace in
    return " "
}

// standardized is "CREDIT 03/02/2022 Payroll from employer $200.23"

或者,也许您想将欧洲格式化的数字转换为 Swift “标准”数字

let standardized = "10.330,99 8.330,22 330,99".replacingAll(.number, locale: Locale(identifier: "en_DE")) { number in
    return NumberFormatter.localizedString(from: number as NSNumber, number: .decimal)
}

// standardized is "10,330.99 8,330.22 330.99")

或者,也许您想将摄氏温度转换为华氏温度

let convertedTemperatures = ["25 °C", "12.5 degrees celsius", "-22.6 C"].replacingAll(.temperature) { celsius in
    
    let measurementC: Measurement<UnitTemperature> = Measurement(value: celsius.value.doubleValue, unit: .celsius)
    let measurementF = measurementC.converted(to: .fahrenheit)
    
    let formatter = MeasurementFormatter()
    formatter.unitOptions = .providedUnit
    return formatter.string(from: measurementF)
    
}

// convertedTemperatures is ["77°F", "54.5°F", "-8.68°F"]

使用您自己的自定义类型扩展 SoulverCore

假设我们有以下格式的字符串,描述一些容器

我们想将这些数据提取到表示 Container 的自定义 Swift 类型中。

  1. 定义我们的模型类(如果它们尚不存在)
enum Color: String, RawRepresentable {
	case blue
	case red
	case yellow
}

enum Size: String, RawRepresentable {
	case small
	case medium
	case large
}

struct Container {
   let color: Color
   let size: Size
   let volume: Decimal

   init(_ data: (Color, Size, UnitExpression)) {
        self.color = data.0
        self.size = data.1
        self.volume = data.2.value
    }
}
  1. 然后为 Color 和 Size 创建解析器,并将它们作为 DataPoint 上的静态变量添加
struct ColorParser: DataFromTokenParser {
    typealias DataType = Color
    
    func parseDataFrom(token: SoulverCore.Token) -> Color? {
        return Color(rawValue: token.stringValue.lowercased())
    }
}

struct SizeParser: DataFromTokenParser {
    typealias DataType = Size

    func parseDataFrom(token: SoulverCore.Token) -> Size? {
        return Size(rawValue: token.stringValue.lowercased())
    }
}

extension DataPoint {
    static var color: DataPoint<ColorParser> {
        return DataPoint<ColorParser>(parser: ColorParser())
    }

    static var size: DataPoint<SizeParser> {
        return DataPoint<SizeParser>(parser: SizeParser())
    }
}
  1. 这就是所有设置。您现在可以从字符串中解析数据,并填充您的模型对象
  let container1 = Container("Color: blue, size: medium, volume: 12.5 cm3".find(.color, .size, .volume)!)
  let container2 = Container("Color: red, size: small, volume: 6.2 cm3".find(.color, .size, .volume)!)
  let container3 = Container("Color: yellow, size: large, volume: 17.82 cm3".find(.color, .size, .volume)!)

在 Swift Regex Builder (5.7 版本中推出) 中将 SoulverCore 用作解析器

SoulverCore 将能够用于解析 Swift regex builder DSL (5.7 版本中推出) 内的数据。这通常比弄清楚如何使用正则表达式匹配您的数据格式更容易。

if #available(macOS 13.0, iOS 16.0, *) {
    let input = "Cost: 365.45, Date: March 12, 2022"
    
    let regex = Regex {
        "Cost: "
        Capture {
            DataPoint<NumberFromTokenParser>.number
        }
        ", Date: "
        Capture {
            DataPoint<DateFromTokenParser>.date
        }
    }
    
    let match = input.wholeMatch(of: regex).1 // 365.45
}

注意:Swift 编译器似乎无法从 DataPoint 上的静态变量推断 DataPoint 泛型参数,这既令人困惑又令人遗憾(有人知道为什么吗?)。

在修复此问题之前,您必须显式指定与您要匹配的数据类型对应的 DataFromTokenParser。

性能

SoulverCore 不太可能成为您应用程序的瓶颈。

在我们的测试中,SoulverCore 在 Intel 上执行约 6k 次操作/秒,在  Silicon 上执行 10k+ 次操作/秒。

虽然这确实不如正则表达式快,但公平地说,SoulverCore 做了更多的工作。在检查您的查询是否匹配之前,SoulverCore 会将整个字符串解析为表示各种数据类型的标记,它可以识别 20 多种数据类型(包括各种格式的日期、数字和单位、地点、时区等等……)。

如果正则表达式也这样做,那将是不可能构建的,即使这样的正则表达式是可能的,它的运行速度也会比 SoulverCore 慢得多。

与其他数据解析方法比较

Apple 的字符串解析工具包包括 Regex、NSScanner 和 NSDataDetector。让我们将这些工具包中的每一个与 SoulverCore 进行比较和对比。

正则表达式

正则表达式 将永远伴随我们,但请自问,您真的想使用它们进行数据处理吗?

它们乍一看不易理解,并且构造正确的正则表达式来匹配数据至少是乏味的(如果不是有时在精神上相当具有挑战性的话)。

正则表达式只“看到”字符/数字/空格集,因此它迫使您考虑要解析的数据的字符串格式,并且通常还要考虑如何跳过通向它的其他字符串。

因此,即使在 Swift 5.7 中对正则表达式进行了重大增强(类型安全的元组匹配和正则表达式构建器语法),正则表达式也使您在错误的抽象级别(即字符,而不是数据类型)考虑数据解析。

如果 Swift 要实现其成为 世界上最伟大的字符串和数据处理语言 的目标,它需要一些在数据抽象级别上更人性化的东西,而不是字符集。

NSScanner

扫描器是一种命令式(而不是声明式)方法,用于从字符串中解析数据。您逐步移动扫描器通过字符串,扫描出您想要的组件。

NSScanner 的一个好处是它能够忽略您不关心的字符串部分。但是,扫描器仍然只知道数字和字符串 - 而不知道更高级别的数据类型。

这是一个 StackOverflow 帖子,说明了如何使用 NSScanner 从字符串 “user logged (3 attempts)” 中扫描整数。

NSString *logString = @"user logged (3 attempts)";
NSString *numberString;
NSScanner *scanner = [NSScanner scannerWithString:logString];
[scanner scanUpToCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] intoString:nil];
[scanner scanCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] intoString:&numberString];
NSLog(@"Attempts: %i", [numberString intValue]); // 3

正则表达式(在 Swift 5.7+ 中)在某种程度上更简洁

if #available(macOS 13.0, iOS 16.0, *) {
    let match = "user logged (3 attempts)".firstMatch(of: /([+\\-]?[0-9]+)/)
    let numberSubstring = match!.0
    let number = Int(numberSubstring)
}

现在是 SoulverCore

let number = "user logged (3 attempts)".find(.number)

NSDataDetector

NSDataDetector 是 NSRegularExpression 的子类,能够扫描字符串中的日期、URL、电话号码、地址和航班详细信息。这是一个很棒的类,支持多种不同的格式。此外,它从字符串返回正确的数据类型,例如 URL 和 Date(非常像 SoulverCore)。

比较

NSDataDetector
let input = "Learn more at https://fascinatingcaptian.com today."
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let url = detector.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))!.url!
SoulverCore
let url = "Learn more at https://fascinatingcaptian.com today".find(.url)

NSDataDetector 的缺点是 API 不是特别 “Swifty”,支持的数据类型有限,并且它不是 Foundation 的平台独立实现的一部分(因此您无法在 Linux、Windows 等上使用它)

许可证

SoulverCore 是一个商业许可的、闭源 Swift 框架。SoulverCore 的标准许可条款适用于其在字符串处理中的使用(请参阅 SoulverCore 许可证)。

对于个人(非商业)项目,您不需要许可证。因此,请继续在您的个人项目中使用这个很棒的库吧!

对于一些商业用例,也提供仅需署名的许可证。