像太平洋西北部的其他人一样,我们周末被雪困住了。为了打发时间,我们决定打开我们的棋盘游戏:卡尔卡松、马奇·库罗、电网、大流行病;我们有很多优秀的选择。但是下午我们被关在家里,我们决定了一个经典:线索。
我,我是《独领风骚》的狂热粉丝——是的,我就是这么称呼它的。因为尽管我在美国出生和长大,但我坚持用它的真名来称呼它,因为在美国,这款游戏的销售和营销完全是以“线索”的名字进行的。(否则,我们如何参考**1985年的电影改编?**)
唉,我无情的迂腐经常导致我错过邀请去玩。如果有人问:
var invitation = "Hey, would you like to play Clue?"invitation.contains("Cluedo") // false
……我不知道他们在说什么。只要他们费心措辞得当,他们的意图是毫无疑问的:
invitation = "Fancy a game of Cluedo™?"invitation.contains("Cluedo") // true
当然,正则表达式将允许我放松我的严格标准。我可以听/Clue吗?™?/,永远不要错过另一个邀请。
但是谁会费心在斯威夫特中找出正则表达式呢?
好吧,削尖你的铅笔,收集你的侦探笔记,预热你的六面骰子,因为本周在NS Hipster上,我们正在破解被称为NS正则表达式的笨重类的案例。
谁在Swift中杀死了正则表达式?
我有一个建议:
在API中,它是具有繁琐可用性的NS正则表达式。
在任何其他语言中,正则表达式都是可以在一行中使用的。
-
需要用一个词代替另一个词吗?
-
Boom**:**正则表达式。
-
需要从模板字符串中提取值吗?
-
Boom**:**正则表达式。
-
需要解析XML?
-
Boom**:
**正则表达式实际上,在这种情况下,您应该真正使用XML解析器
但是在Swift中,您必须经历初始化NS Reg rar Express对象和从String范围到NS Range值来回转换的麻烦。这完全是一种拖累。
好消息是:
-
在Swift中使用正则表达式不需要NS正则表达式。
-
最近在Swift 4和5中添加的功能使您在需要时使用NS正则表达式变得非常非常好。
让我们依次询问以下每一点:
没有NS正则表达式
你可能会惊讶地发现,事实上,你可以在Swift一行中使用正则表达式——你只需要完全绕过NS正则表达式。
匹配字符串与模式
导入Foundation框架时,Swift String类型自动获得对NS String实例方法和初始化器的访问权限。其中包括range(of:选项: range: locale:),它查找并返回指定字符串的第一个范围。
通常情况下,这执行按书搜索子字符串的操作*。Meh.*
但是,如果您传递。正则表达式选项,字符串参数将作为模式匹配。尤里卡!
让我们利用这个鲜为人知的功能,将我们的“线索”意识转向*“美国*”环境。
import Foundationlet invitation = "Fancy a game of Cluedo™?"invitation.range(of: #"\bClue(do)?™?\b"#, options: .regularExpression) != nil // true
如果模式与指定的字符串匹配,则该方法返回Range<String。索引>对象。因此,检查非零值会告诉我们是否发生了匹配。
方法本身为选项、范围和区域设置参数提供默认参数;默认情况下,它对当前区域设置中的整个字符串执行本地化的无保留搜索。
在正则表达式中,?运算符匹配前面的字符或组零次或一次。我们在模式中使用它,使“线索”中的“-do”成为可选的(既能容纳美式拼写,也能容纳正确的拼写),并允许任何希望一本正经的人使用商标符号(™)。
如果当前位置是单词边界,则\b元字符匹配,这发生在单词(\w)和非单词(\W)字符之间。将我们的模式固定在单词边界上以匹配,可以防止“伪线索”等误报。
这解决了我们错过邀请的问题。下一个问题是如何以同样的方式回应。
搜索和检索匹配
我们实际上可以使用返回值来查看匹配的字符串,而不仅仅是检查非零值。
import Foundationfunc respond(to invitation: String) { if let range = invitation.range(of: #"\bClue(do)?™?\b"#, options: .regularExpression) { switch invitation[range] { case "Cluedo": print("I'd be delighted to play!") case "Clue": print("Did you mean Cluedo? If so, then yes!") default: fatalError("(Wait... did I mess up my regular expression?)") } } else { print("Still waiting for an invitation to play Cluedo.") }}
方便的是,range(of:...) 方法返回的范围可以插入下标,以获得匹配范围的子字符串。
查找和替换匹配
一旦我们确定游戏已经开始,下一步就是阅读说明。尽管它相对简单,但玩家经常忘记《线索》中的重要规则,比如需要在房间里才能提出建议。)
自然,我们播放原版的英国版。但作为对美国玩家的帮助,我会不厌其烦地立即将规则本地化。例如,最初版本中受害者的名字是“布莱克博士”,但在美国是“博迪先生”。
我们使用替换事件(of: with:选项:)方法来自动化这个过程——再次传递。正则表达式选项。
import Foundationlet instructions = """The object is to solve by means of elimination and deductionthe problem of the mysterious murder of Dr. Black."""instructions.replacingOccurrences( of: #"(Dr\.|Doctor) Black"#, with: "Mr. Boddy", options: .regularExpression)
正则表达式
我们可以使用range(of:选项: range: locale:)方法实现的目标是有限制的,并且替换出现(of: with:选项:)方法实现的目标也是有限制的。
具体来说,如果您想在字符串中多次匹配模式或从捕获组中提取值,您需要使用NS正则表达式。
枚举具有位置捕获组的匹配
正则表达式可以在字符串上匹配其模式一次或多次。在每个匹配中,可能有一个或多个捕获组,它们由正则表达式模式中的括号包围指定。
例如,假设您想使用正则表达式来确定需要多少玩家来玩独领风骚:
import Foundationlet description = """Cluedo is a game of skill for 2-6 players."""let pattern = #"(\d+)[ \p{Pd}](\d+) players"#let regex = try NSRegularExpression(pattern: pattern, options: [])
该模式包括一个或多个数字的两个捕获组,分别由+运算符和\d元字符表示。
在它们之间,我们在包含空格和**Unicode通用类别**Pd(标点符号,破折号)中的任何字符的集合上进行匹配。这使我们能够匹配连字符/减去 (-), en dash (–), em dash (—), 或任何其他我们可能遇到的外来印刷标记。
我们可以使用enumerate Matches(in: ption: range)方法尝试每个匹配,直到找到一个具有三个范围(整个匹配和两个捕获组)的匹配,其捕获的值可用于初始化有效范围。在所有这些过程中,我们使用新的(-ish)NS Range(: in:)和Range(: in:)初始化器在String和NS String索引范围之间进行转换。一旦找到这样的匹配,我们将第三个闭包参数(指向布尔值的指针)设置为true,作为告诉枚举停止的一种方式。
var playerRange: ClosedRange<Int>?let nsrange = NSRange(description.startIndex..<description.endIndex, in: description)regex.enumerateMatches(in: description, options: [], range: nsrange) { (match, _, stop) in guard let match = match else { return } if match.numberOfRanges == 3, let firstCaptureRange = Range(match.range(at: 1), in: description), let secondCaptureRange = Range(match.range(at: 2), in: description), let lowerBound = Int(description[firstCaptureRange]), let upperBound = Int(description[secondCaptureRange]), lowerBound > 0 && lowerBound < upperBound { playerRange = lowerBound...upperBound stop.pointee = true }}print(playerRange!)// Prints "2...6"
通过调用匹配对象上的range(at:)方法,可以按位置访问每个捕获组。
*叹息。什么呀?*不,我们喜欢我们想出的解决方案——尽管冗长。只是……天哪,如果我们能单独玩独领风骚不是很好吗?
使用命名捕获组匹配多行模式
唯一一件让《线索》成为严格意义上的多人游戏的事情是,你需要某种方法来测试一个理论,而不会向自己透露答案。
如果我们想写一个程序来检查它,而不破坏任何东西,第一步是将一个建议解析成它的组成部分:嫌疑人、位置和武器。
let suggestion = """I suspect it was Professor Plum, \in the Dining Room, \with the Candlestick."""
当编写复杂的正则表达式时,准确地知道您的平台支持哪些功能会有所帮助。在Swift的例子中,NS**正则表达式是一个围绕着正则表达式引擎**的包装器,它让我们可以做一些非常好的事情:
let pattern = #"""(?xi)(?<suspect> ((Miss|Ms\.) \h Scarlett?) | ((Colonel | Col\.) \h Mustard) | ((Reverend | Mr\.) \h Green) | (Mrs\. \h Peacock) | ((Professor | Prof\.) \h Plum) | ((Mrs\. \h White) | ((Doctor | Dr\.) \h Orchid))),?(?-x: in the )(?<location> Kitchen | Ballroom | Conservatory | Dining \h Room | Library | Lounge | Hall | Study),?(?-x: with the )(?<weapon> Candlestick | Knife | (Lead(en)?\h)? Pipe | Revolver | Rope | Wrench)"""#let regex = try NSRegularExpression(pattern: pattern, options: [])
首先,用多行原始字符串文字声明模式在可读性方面是一个巨大的胜利。这与这些组中的x和i标志相结合,允许我们使用空白来组织我们的表达式,使其更容易理解。
另一个细节是这个模式如何使用命名的捕获组(由(?)),而不是前面示例中的标准位置捕获组。这样做允许我们通过调用匹配对象上的range(with Name:)方法按名称访问组。
除了更古怪的策略之外,我们还能承受地区差异,包括“斯嘉丽小姐”的拼写、“/牧师格林先生”的头衔,以及2016年后标准版中怀特夫人被兰花博士取代。
let nsrange = NSRange(suggestion.startIndex..<suggestion.endIndex, in: suggestion)if let match = regex.firstMatch(in: suggestion, options: [], range: nsrange){ for component in ["suspect", "location", "weapon"] { let nsrange = match.range(withName: component) if nsrange.location != NSNotFound, let range = Range(nsrange, in: suggestion) { print("\(component): \(suggestion[range])") } }}// Prints:// "suspect: Professor Plum"// "location: Dining Room"// "weapon: Candlestick"
正则表达式是处理文本的强大工具,但如何在Swift中使用它们通常是个谜。我们希望这篇文章能帮助您找到解决方案。