├── .github
├── pull_request_template.md
└── workflows
│ └── swift.yml
├── .gitignore
├── .spi.yml
├── .swiftpm
├── configuration
│ └── Package.resolved
└── xcode
│ └── xcshareddata
│ └── xcschemes
│ └── NativeRegexExamples.xcscheme
├── LICENSE
├── NativeRegexExamples.xctestplan
├── Package.resolved
├── Package.swift
├── Package@swift-6.0.swift
├── README.md
├── Sources
└── NativeRegexExamples
│ ├── DataTypes
│ ├── Date.swift
│ ├── IPv4.swift
│ ├── Phone Numbers.swift
│ ├── SSN.swift
│ └── email.swift
│ ├── Documentation.docc
│ ├── Getting Started.md
│ └── Using the Test Suite.md
│ ├── RegexActor.swift
│ ├── RegexTestSuite protocol.swift
│ └── name spaces.swift
└── Tests
└── NativeRegexExamplesTests
├── DateTests.swift
├── EmailTests.swift
├── IPv4Tests.swift
├── PhoneNumbers
├── Experimental
│ ├── PhoneNumberDataDetectorTests.swift
│ └── PhoneNumberKitParser.swift
└── PhoneNumberTests.swift
└── SSNTests.swift
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Pull Request Template
2 |
3 | ## Description
4 |
5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
6 |
7 | Fixes # (issue)
8 |
9 | ## Type of change
10 |
11 | Please delete options that are not relevant.
12 |
13 | - [ ] Bug fix (non-breaking change which fixes an issue)
14 | - [ ] New feature (non-breaking change which adds functionality)
15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
16 | - [ ] This change requires a documentation update
17 |
18 | **Test Configuration**:
19 | * Firmware version:
20 | * Hardware:
21 | * Toolchain:
22 | * SDK:
23 |
24 | ## Checklist:
25 | - [ ] I have performed a self-review of my own code
26 | - [ ] I have commented my code, particularly in hard-to-understand areas
27 | - [ ] I have made corresponding changes to the documentation
28 | - [ ] My changes generate no new warnings
29 | - [ ] If I have added a new `Regex`, have I:
30 | - [ ] followed the same structure as the other `Regex` in the library.
31 | - [ ] Added a literal version under the `RegexLiterals` namespace.
32 | - [ ] Added a regex builder version under the `RegexBuilders` namespace.
33 | - Note: I understand that it can be prohibitively time consuming and difficult to implement a Regex, let alone implement it in two very different syntaxes (literal and RegexBuilder). This is why I highly recommend copying and pasting your `Regex` into [swiftregex.com](https://www.swiftregex.com). It can immediately convert back and forth between both syntaxes.
34 | - [ ] I have added tests that prove my change is effective
35 | - [ ] If I have added a new `Regex`, then it is covered by a new test suite conforming to the `RegexTestSuite` protocol.
36 | - [ ] I have added multiple test strings. (See other tests for examples.)
37 | - [ ] I have marked any failing tests with `withKnownIssues`
38 | - [ ] New and existing unit tests pass locally with my changes
39 | - [ ] Any dependent changes have been merged and published in downstream modules
40 | - [ ] I have checked my code and corrected any misspellings
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | workflow_dispatch:
5 | jobs:
6 | # main:
7 | # name: Build & Test (Swift v${{ matrix.swift }} on ${{ matrix.os }})
8 | # runs-on: ${{ matrix.os }}
9 | # strategy:
10 | # matrix:
11 | # os: [ubuntu-latest]
12 | # swift: ["5"]
13 | # steps:
14 | # - name: Checkout
15 | # uses: actions/checkout@v4
16 | # - name: "Use Swift v${{ matrix.swift }}"
17 | # uses: swift-actions/setup-swift@v2
18 | # with:
19 | # swift-version: ${{ matrix.swift }}
20 | # - name: Display Swift version
21 | # run: swift --version
22 | # - name: Build
23 | # run: swift build
24 | # Testing only supported with Swift >= 6.0
25 | testing:
26 | name: Build & Test (beta)
27 | runs-on: macos-14
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v4
31 | - name: Use Swift v6.0
32 | uses: maxim-lobanov/setup-xcode@v1
33 | with:
34 | xcode-version: "16.0"
35 | - name: Build
36 | run: swift build
37 | - name: Test
38 | run: swift test --enable-swift-testing
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 | /.vscode
10 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [NativeRegexExamples]
--------------------------------------------------------------------------------
/.swiftpm/configuration/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "3abce946d7d39337100fed5938f428ba7e161c6175e38ec9e84d5c553dcc8467",
3 | "pins" : [
4 | {
5 | "identity" : "swift-custom-dump",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
8 | "state" : {
9 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
10 | "version" : "1.3.3"
11 | }
12 | },
13 | {
14 | "identity" : "xctest-dynamic-overlay",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
17 | "state" : {
18 | "revision" : "96beb108a57f24c8476ae1f309239270772b2940",
19 | "version" : "1.2.5"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/NativeRegexExamples.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
49 |
50 |
51 |
52 |
54 |
60 |
61 |
62 |
63 |
64 |
74 |
75 |
81 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Daniel Lyons
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NativeRegexExamples.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "75F3603A-BFE4-43CD-8085-4EFA6AFC5B3E",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 |
13 | },
14 | "testTargets" : [
15 | {
16 | "target" : {
17 | "containerPath" : "container:",
18 | "identifier" : "NativeRegexExamplesTests",
19 | "name" : "NativeRegexExamplesTests"
20 | }
21 | }
22 | ],
23 | "version" : 1
24 | }
25 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "6b0f20e9a6016e99429d6d9691547a7c72d94cf89c81eedd51e8aa36fd210679",
3 | "pins" : [
4 | {
5 | "identity" : "phonenumberkit",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/marmelroy/PhoneNumberKit.git",
8 | "state" : {
9 | "revision" : "28aee1bd9d4a8fa46304c598b73231b043161e63",
10 | "version" : "4.0.0"
11 | }
12 | },
13 | {
14 | "identity" : "swift-custom-dump",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
17 | "state" : {
18 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
19 | "version" : "1.3.3"
20 | }
21 | },
22 | {
23 | "identity" : "xctest-dynamic-overlay",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
26 | "state" : {
27 | "revision" : "96beb108a57f24c8476ae1f309239270772b2940",
28 | "version" : "1.2.5"
29 | }
30 | }
31 | ],
32 | "version" : 3
33 | }
34 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.10
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "NativeRegexExamples",
8 | platforms: [.iOS(.v16), .macOS(.v13), .macCatalyst(.v16), .tvOS(.v16), .visionOS(.v1), .watchOS(.v9)],
9 | dependencies: [
10 | .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3"), // Custom Dump
11 | .package(url: "https://github.com/marmelroy/PhoneNumberKit.git", from: "4.0.0"), // PhoneNumberKit
12 | ],
13 | products: [
14 | // Products define the executables and libraries a package produces, making them visible to other packages.
15 | .library(
16 | name: "NativeRegexExamples",
17 | targets: ["NativeRegexExamples"]),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package, defining a module or a test suite.
21 | // Targets can depend on other targets in this package and products from dependencies.
22 | .target(
23 | name: "NativeRegexExamples",
24 | dependencies: [
25 | .product(name: "PhoneNumberKit", package: "phonenumberkit"), // also available: PhoneNumberKit-Static, PhoneNumberKit-Dynamic
26 | ],
27 | swiftSettings: [
28 | .enableUpcomingFeature("BareSlashRegexLiterals"),
29 | .enableExperimentalFeature("StrictConcurrency"),
30 | ]
31 | ),
32 | // "NativeRegexExamplesTests" is only available on Swift 6 as it requires Swift Testing
33 | ],
34 | swiftLanguageVersions: [.v5]
35 | )
36 |
--------------------------------------------------------------------------------
/Package@swift-6.0.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "NativeRegexExamples",
8 | platforms: [.iOS(.v16), .macOS(.v13), .macCatalyst(.v16), .tvOS(.v16), .visionOS(.v1), .watchOS(.v9)],
9 | products: [
10 | // Products define the executables and libraries a package produces, making them visible to other packages.
11 | .library(
12 | name: "NativeRegexExamples",
13 | targets: ["NativeRegexExamples"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3"), // Custom Dump
17 | .package(url: "https://github.com/marmelroy/PhoneNumberKit.git", from: "4.0.0"), // PhoneNumberKit
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package, defining a module or a test suite.
21 | // Targets can depend on other targets in this package and products from dependencies.
22 | .target(
23 | name: "NativeRegexExamples",
24 | dependencies: [
25 | .product(name: "CustomDump", package: "swift-custom-dump"), // CustomDump
26 | .product(name: "PhoneNumberKit", package: "phonenumberkit"), // also available: PhoneNumberKit-Static, PhoneNumberKit-Dynamic
27 | ]
28 | ),
29 | .testTarget(
30 | name: "NativeRegexExamplesTests",
31 | dependencies: [
32 | "NativeRegexExamples",
33 | .product(name: "CustomDump", package: "swift-custom-dump"),
34 | ]
35 | ),
36 | ],
37 | swiftLanguageModes: [.v6, .v5]
38 | )
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NativeRegexExamples
2 | [](https://swiftpackageindex.com/DandyLyons/NativeRegexExamples)
3 | [](https://swiftpackageindex.com/DandyLyons/NativeRegexExamples)
4 |
5 | ## I'm Job Hunting
6 | 👋🏼 My name is Daniel Lyons, I'm a Swift developer, and I'm currently looking for a job. I hope this package is helpful for you and the community. If so, **please consider viewing and sharing my developer site with others**: [dandylyons.github.io](https://dandylyons.github.io). 🙏🏼
7 |
8 | ## Purpose
9 | **NativeRegexExamples** is a place for the Swift community to:
10 | 1. **crowd-source** `Regex` solutions that can be used in your projects
11 | 2. **learn** from each other and develop best practices
12 | - Provide cheat sheets
13 | 3. **test** Regexes for:
14 | - matches: so we can assess their capabilities
15 | - non-matches: so we can eliminate false positives
16 | - replacing capabilities
17 |
18 | ## Basic Usage
19 | ```swift
20 | @RegexActor
21 | func foo() {
22 | let ssnRegex = RegexLiterals.ssn
23 | let string = "111-11-1111"
24 | string.contains(ssnRegex) // true
25 | string.wholeMatch(of: ssnRegex)
26 |
27 | var text = """
28 | one SSN -> 111-11-1111
29 | 222-22-2222 <- another SSN
30 | """
31 | text.replace(ssnRegex, with: "___")
32 | // text is now:
33 | // one SSN -> ___
34 | // ___ <- another SSN
35 | }
36 | ```
37 |
38 | Don't just use the library. Have a look at the source code so that you can learn from it. Each regex has a literal definition and a RegexBuilder definition. For example:
39 | ```swift
40 | public extension RegexLiterals {
41 | static let ssn = #/
42 | # Area number: Can't be 000-199 or 666
43 | (?!0{3})(?!6{3})[0-8]\d{2}
44 | -
45 | # Group number: Can't be 00
46 | (?!0{2})\d{2}
47 | -
48 | # Serial number: Can't be 0000
49 | (?!0{4})\d{4}
50 | /#
51 | }
52 |
53 | public extension RegexBuilders {
54 | static let ssn = Regex {
55 | NegativeLookahead {
56 | Repeat(count: 3) {
57 | "0"
58 | }
59 | }
60 | NegativeLookahead {
61 | Repeat(count: 3) {
62 | "6"
63 | }
64 | }
65 | ("0"..."8")
66 | Repeat(count: 2) {
67 | One(.digit)
68 | }
69 | "-"
70 | NegativeLookahead {
71 | Repeat(count: 2) {
72 | "0"
73 | }
74 | }
75 | Repeat(count: 2) {
76 | One(.digit)
77 | }
78 | "-"
79 | NegativeLookahead {
80 | Repeat(count: 4) {
81 | "0"
82 | }
83 | }
84 | Repeat(count: 4) {
85 | One(.digit)
86 | }
87 | }
88 | .anchorsMatchLineEndings()
89 | }
90 | ```
91 |
92 | ## Motivation
93 | Regular expressions are an extremely powerful tool capable of complex pattern matching, validation, parsing and so many more things. Nevertheless, it can be quite difficult to use, and it has a very esoteric syntax that is extremely easy to mess up. Every language has it's own "flavor" of Regex, and Swift's improves in some significant ways:
94 |
95 | 1. Strict compile time type-checking
96 | 2. Syntax highlighting for Regex literals
97 | 3. An optional, more readable, DSL through RegexBuilder
98 |
99 | However, many Swift resources about Regular expressions are about older technologies such as `NSRegularExpressions`, or third-party Swifty libraries. While these technologies and resources are great, they don't give us a chance to learn and unlock the new capabilities of native Swift Regex.
100 |
101 | Regex is also a decades-old technology. This means that many problems have long ago been solved in regular expressions. Better yet, Swift `Regex` literals are designed so that they are compatible with many other language flavors of regex including Perl, Python, Ruby, and Java. We might as well learn from the experiences of other communities!
102 |
103 | ## Contributing
104 | Contributions are greatly appreciated for the benefit of the Swift community. Please feel free to file a PR or Issue!
105 |
106 | All data types should have tests added. Testing is done entirely through the new [Swift Testing](https://developer.apple.com/xcode/swift-testing/) framework. This should ensure, that the library is usable/testable on non-Xcode, non-Apple platforms in the future.
107 |
108 | Sorry, Swift Testing is Swift 6 and up only. Though, I see no reason why we shouldn't be able to backdeploy the library to 5.7 and up.
109 |
110 | ### Recommended Resources
111 | I strongly recommend using [swiftregex.com](https://swiftregex.com/) by [SwiftFiddle](https://github.com/SwiftFiddle). It's a powerful online playground for testing Swift `Regex`es. One of it's best features is that it can convert back and forth from traditional regex patterns and Swift's RegexBuilder DSL.
112 |
113 | ## Inspirations
114 | - [RegExLib.com](https://regexlib.com/Default.aspx) is one of many sites that crowd-sources regular expressions. It also, tests regular expressions for matches and non-matches
115 | - [iHateRegex.com](https://ihateregex.io/playground) can visualize regular expression logic.
116 |
117 | ## Gotchas
118 | ### Strict Concurrency Checking
119 | The Swift `Regex` type is not `Sendable`. Apparently, this is because `Regex` allows users to hook in their own custom logic so Swift cannot guarantee data race safety. For this reason, I have made all the `Regex`es in the library isolated to `@RegexActor`, (a minimal global actor defined in the library). If I can find a better solution I will remove this actor isolation. If you use any regex from the library in your code directly, you will most likely need to isolate to `@RegexActor`. That being said, you should be able to copy and paste any regex in the library into your own code, and then you will no longer be limited to `@RegexActor`.
120 |
121 | ## Recommended Resources
122 |
123 | ### Swift Regex
124 | - [WWDC22 Meet Swift Regex](https://developer.apple.com/videos/play/wwdc2022/110357/)
125 | - [WWDC22 Swift Regex: Beyond the basics](https://developer.apple.com/videos/play/wwdc2022/110358)
126 |
127 | ### Swift Testing
128 | - Video series: [Swift and Tips | Mastering Swift Testing series](https://www.youtube.com/watch?v=zXjM1cFUwW4&list=PLHWvYoDHvsOV67md_mU5nMN_HDZK7rEKn&pp=iAQB)
129 | - [Mastering the Swift Testing Framework | Fatbobman's Blog](https://fatbobman.com/en/posts/mastering-the-swift-testing-framework/#parameterized-testing)
130 | - I'm taking copious [notes](https://dandylyons.github.io/notes/Topics/Software-Development/Programming-Languages/Swift/testing-in-Swift/swift-testing) on `swift-testing` here.
131 |
132 | ## Installation
133 | Add NativeRegexExamples as a package dependency in your project's Package.swift:
134 |
135 | ```swift
136 | // swift-tools-version:6.0
137 | import PackageDescription
138 |
139 | let package = Package(
140 | name: "MyPackage",
141 | dependencies: [
142 | .package(
143 | url: "https://github.com/DandyLyons/NativeRegexExamples",
144 | .upToNextMinor(from: "0.0.1")
145 | )
146 | ],
147 | targets: [
148 | .target(
149 | name: "MyTarget",
150 | dependencies: [
151 | .product(name: "NativeRegexExamples", package: "NativeRegexExamples")
152 | ]
153 | )
154 | ]
155 | )
156 | ```
157 |
158 | ## Project Status
159 | The project is in an early development phase. Current goals:
160 |
161 | - [ ] **More examples with passing tests**: Increase examples to all common use cases of regular expressions
162 | - [ ] **Documentation**: Ensure accuracy and completeness of documentation and include code examples.
163 |
164 | Your contributions are very welcome!
165 |
166 | ## Thank Yous
167 | - the **iOS Code Review** newsletter for featuring us in [Issue #71](https://ioscodereview.com/issues/71/).
168 |
169 | ## Star History
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | ## License
180 | This project is licensed under the MIT License. See the [LICENSE file](https://github.com/DandyLyons/NativeRegexExamples/blob/main/LICENSE) for details.
181 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/DataTypes/Date.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 | import Foundation
3 |
4 | public extension RegexLiterals {
5 | /// A Regex literal which parses dates in the MM/DD/YYYY format.
6 | ///
7 | /// This regex is provided for learning purposes, but it is much better to use the regex parsers that ship in the
8 | /// Foundation library as they support a wide variety of formats and they account for locale.
9 | static let date_MM_DD_YYYY = #/\b(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])/(\d{4})\b/#
10 | }
11 |
12 | public extension RegexBuilders {
13 | /// A Regex literal which parses dates in the MM/DD/YYYY format.
14 | ///
15 | /// This regex is provided for learning purposes, but it is much better to use the regex parsers that ship in the
16 | /// Foundation library as they support a wide variety of formats and they account for locale.
17 | static let date_MM_DD_YYYY = Regex {
18 | Anchor.wordBoundary
19 | ChoiceOf {
20 | Regex {
21 | "0"
22 | ("1"..."9")
23 | }
24 | Regex {
25 | "1"
26 | ("0"..."2")
27 | }
28 | }
29 | "/"
30 | ChoiceOf {
31 | Regex {
32 | "0"
33 | ("1"..."9")
34 | }
35 | Regex {
36 | One(.anyOf("12"))
37 | ("0"..."9")
38 | }
39 | Regex {
40 | "3"
41 | One(.anyOf("01"))
42 | }
43 | }
44 | "/"
45 | Repeat(count: 4) {
46 | One(.digit)
47 | }
48 | Anchor.wordBoundary
49 | }
50 | .anchorsMatchLineEndings()
51 |
52 | /// An example of using one of Foundation's built in date parser.
53 | ///
54 | ///
55 | static let iso8601: Regex<(Date)> = Regex {
56 | One(
57 | .iso8601(
58 | timeZone: .gmt,
59 | includingFractionalSeconds: false,
60 | dateSeparator: .dash,
61 | dateTimeSeparator: .standard,
62 | timeSeparator: .omitted
63 | )
64 | )
65 | }
66 | }
67 |
68 | extension Locale {
69 | public static let en_US: Self = .init(identifier: "en_US")
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/DataTypes/IPv4.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 |
3 | public extension RegexLiterals {
4 | static let ipv4: Regex = #/
5 | (?:\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}
6 | /#
7 | }
8 |
9 |
10 | public extension RegexBuilders {
11 | static let ipv4 = Regex {
12 | ChoiceOf {
13 | Regex {
14 | Anchor.wordBoundary
15 | "25"
16 | ("0"..."5")
17 | }
18 | Regex {
19 | Anchor.wordBoundary
20 | "2"
21 | ("0"..."4")
22 | ("0"..."9")
23 | }
24 | Regex {
25 | Anchor.wordBoundary
26 | Optionally(.anyOf("01"))
27 | ("0"..."9")
28 | Optionally(("0"..."9"))
29 | }
30 | }
31 | Repeat(count: 3) {
32 | Regex {
33 | "."
34 | ChoiceOf {
35 | Regex {
36 | "25"
37 | ("0"..."5")
38 | }
39 | Regex {
40 | "2"
41 | ("0"..."4")
42 | ("0"..."9")
43 | }
44 | Regex {
45 | Optionally(.anyOf("01"))
46 | ("0"..."9")
47 | Optionally(("0"..."9"))
48 | }
49 | }
50 | }
51 | }
52 | }
53 | .anchorsMatchLineEndings()
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/DataTypes/Phone Numbers.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import IssueReporting
3 | import RegexBuilder
4 |
5 | public extension RegexLiterals {
6 | /// A regex that identifies phone numbers.
7 | ///
8 | /// Have a look at the source code for this regex. It's a great example of Swift's extended delimiter literal
9 | /// syntax. In this syntaax, whitespace is ignored and comments can be added, meaning complex
10 | /// regex syntax can be used by split up in a way that is far more readable.
11 | static let phoneNumber = #/
12 | (?:\+\d{1,3}\s?)? # Optional international code
13 | (?:\d{1,4}[-.\s]?)? # Optional country or area code
14 | \d{1,4}[-.\s]? # Area code or local part
15 | \d{1,4}[-.\s]? # Local part or line number
16 | \d{1,4} # Line number
17 | /#
18 | }
19 |
20 | public extension RegexBuilders {
21 | static let phoneNumber = Regex {
22 | Optionally {
23 | Regex {
24 | "+"
25 | Repeat(1...3) {
26 | One(.digit)
27 | }
28 | Optionally(.whitespace)
29 | }
30 | }
31 | Optionally {
32 | Regex {
33 | Repeat(1...4) {
34 | One(.digit)
35 | }
36 | Optionally {
37 | CharacterClass(
38 | .anyOf("-."),
39 | .whitespace
40 | )
41 | }
42 | }
43 | }
44 | Repeat(1...4) {
45 | One(.digit)
46 | }
47 | Optionally {
48 | CharacterClass(
49 | .anyOf("-."),
50 | .whitespace
51 | )
52 | }
53 | Repeat(1...4) {
54 | One(.digit)
55 | }
56 | Optionally {
57 | CharacterClass(
58 | .anyOf("-."),
59 | .whitespace
60 | )
61 | }
62 | Repeat(1...4) {
63 | One(.digit)
64 | }
65 | }
66 | .anchorsMatchLineEndings()
67 |
68 | }
69 |
70 |
71 | // MARK: PhoneNumberDataDetector
72 | /// Experimental detector of phone numbers backed by `NSDataDetector`.
73 | ///
74 | /// This is experimental and not yet ready for production use.
75 | /// The phone number validation method used by `NSDataDetector` does not appear to
76 | /// have follow a documented standard such as E.164, NANP, or ITU-T E.212.
77 | /// As such, we can't predict what the output will look like. Apple does not
78 | /// publicly document the algorithm used for phone number detection, so it is
79 | /// not possible to deterministically predict what phone numbers will be matched.
80 | @_spi(Experimental)
81 | public struct PhoneNumberDataDetector: CustomConsumingRegexComponent {
82 | public typealias RegexOutput = String
83 | public func consuming(
84 | _ input: String,
85 | startingAt index: String.Index,
86 | in bounds: Range
87 | ) throws -> (upperBound: String.Index, output: String)? {
88 | var result: (upperBound: String.Index, output: String)?
89 |
90 | let types: NSTextCheckingResult.CheckingType = [.phoneNumber]
91 | let detector = try NSDataDetector(types: types.rawValue)
92 | let swiftRange = index..
163 | ) throws -> (upperBound: String.Index, output: String)? {
164 | var result: (upperBound: String.Index, output: String)?
165 |
166 | let phoneNumberUtility = PhoneNumberUtility()
167 | let phoneNumber = try phoneNumberUtility.parse(
168 | input,
169 | withRegion: region,
170 | ignoreType: ignoreType
171 | )
172 |
173 | result = (upperBound: input.endIndex, output: phoneNumber.numberString)
174 | return result
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/DataTypes/SSN.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 |
3 | public extension RegexLiterals {
4 | static let ssn = #/
5 | # Area number: Can't be 000-199 or 666
6 | (?!0{3})(?!6{3})[0-8]\d{2}
7 | -
8 | # Group number: Can't be 00
9 | (?!0{2})\d{2}
10 | -
11 | # Serial number: Can't be 0000
12 | (?!0{4})\d{4}
13 | /#
14 | }
15 |
16 | public extension RegexBuilders {
17 | static let ssn = Regex {
18 | NegativeLookahead {
19 | Repeat(count: 3) {
20 | "0"
21 | }
22 | }
23 | NegativeLookahead {
24 | Repeat(count: 3) {
25 | "6"
26 | }
27 | }
28 | ("0"..."8")
29 | Repeat(count: 2) {
30 | One(.digit)
31 | }
32 | "-"
33 | NegativeLookahead {
34 | Repeat(count: 2) {
35 | "0"
36 | }
37 | }
38 | Repeat(count: 2) {
39 | One(.digit)
40 | }
41 | "-"
42 | NegativeLookahead {
43 | Repeat(count: 4) {
44 | "0"
45 | }
46 | }
47 | Repeat(count: 4) {
48 | One(.digit)
49 | }
50 | }
51 | .anchorsMatchLineEndings()
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/DataTypes/email.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 |
3 | public extension RegexLiterals {
4 | static let email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/
5 | }
6 |
7 | public extension RegexBuilders {
8 | static let email = Regex {
9 | OneOrMore {
10 | CharacterClass(
11 | .anyOf("._%+-"),
12 | ("A"..."Z"),
13 | ("a"..."z"),
14 | ("0"..."9")
15 | )
16 | }
17 | "@"
18 | OneOrMore {
19 | CharacterClass(
20 | .anyOf(".-"),
21 | ("A"..."Z"),
22 | ("a"..."z"),
23 | ("0"..."9")
24 | )
25 | }
26 | "."
27 | Repeat(2...) {
28 | CharacterClass(
29 | ("A"..."Z"),
30 | ("a"..."z")
31 | )
32 | }
33 | }
34 | .anchorsMatchLineEndings()
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/Documentation.docc/Getting Started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Overview
4 | Welcome to NativeRegexExamples! This package serves as a helpful repository of regular expressions in the Swift language. These regular expressions are free to be used in your own projects. You can import the entire library. Or you can simply copy and paste only the regular expressions that you need. (This way you can avoid adding an unnecessary dependency.)
5 |
6 | This library also hosts an extensive test suite. Here you can plainly see the strengths and weaknesses of each regular expression. Learn what matches and does not match. The test suite also includes tests to identify false-positives and false-negatives
7 |
8 | >To learn more see: .
9 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/Documentation.docc/Using the Test Suite.md:
--------------------------------------------------------------------------------
1 | # Testing Suite
2 | Test the effectiveness of your regular expressions.
3 |
4 |
5 | ## What Are We Testing?
6 | When using regular expressions, we want to know what are the capabilities and limitations. In other words, we want to know **what is it that the Regex CAN do?** and **what is it that the Regex CANNOT do?** For this problem space, it is helpful to think in terms of the Confusion Matrix.
7 |
8 | 
9 |
10 | For our purposes, "Actual" will refer to the actual string that is being assessed by the Regex, and "Predicted" refers to whether or not the Regex matched the the string.
11 |
12 | To illustrate, let's say we have a set of strings, and we want to determine if they are valid phone numbers. Some of these strings are **actually a phone number**. We'll call those "actual positives". Next, some of the strings, are not valid phone numbers. (They are **not actually a phone number**.) We'll call those "actual negatives".
13 |
14 | Then each of these strings will be evaluated by a Regex. The Regex will match (or predict) if that string is a phone number. If it matches, then we can call that a "predicted positive" and if it does not match then we can call it a "predicted negative". But we of course need to be aware that the Regex could be accurate or inaccurate. This is where the Confusion Matrix comes in.
15 |
16 | If the Regex's prediction **matches actual** reality, then it is "true". If it **does not match actual** reality, then it is "false". We want to maximize "true" results and minimize "false" results. So we will design the tests so that they pass when the results are "true" and fail when the results are "false".
17 |
18 | ## Reading the Tests
19 | Reading the tests are quite simple, but they might behave slightly differently than other test suites that you are familiar with. Typically with a test suite, we simply want all of our tests to pass. Any test failure is bad, period.
20 |
21 | However, regular expressions are rarely perfect. There are certain "happy path" cases that they will match every time. And there are certain "edge cases" that they will fail at. The regular expression could fail in one of two ways, it could incorrectly label something a match (a "false positive"), or it could incorrectly label something as not matching (a "false negative"). Both of these cases are bad and covered by the test suite.
22 |
23 | Since the results are "false" (meaning they are a "false positive" or a "false negative"), they should be marked in a way that shows that they are false. But we do not want a test failure. Instead we mark it as a known issue.
24 |
25 | ## Adding Tests
26 | To add a test, I recommend that you first look over some of the other tests. Each test should follow the same format to ensure readability and test coverage.
27 |
28 | ### The Test Suite Struct
29 | Each test should be in a struct which will act as the
30 |
31 | ### RegexTestSuite protocol
32 | The ``RegexTestSuite`` protocol serves a few purposes:
33 |
34 | 1. It conveniently adds the ``RegexActor`` global actor. This means all of your tests will be concurrency-safe by default.
35 | 2. It enforces that each test suite covers the same test methods, thus ensuring that the library has uniform test coverage.
36 |
37 | - Note: As good as this protocol is, it cannot guarantee perfect uniform test coverage. This is because the `Swift Testing` framework uses Swift macros, and currently Swift protocols do not have the ability to require macro usage. (If you have a better solution, please raise a PR. Also have a look at [Issue #1](https://github.com/DandyLyons/NativeRegexExamples/issues/1)).
38 |
39 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/RegexActor.swift:
--------------------------------------------------------------------------------
1 | /// A global actor for isolating `Regex`es
2 | ///
3 | /// Unfortunately, `Regex` is not `Sendable` which means we must isolate our library `Regex`s.
4 | @globalActor public actor RegexActor: GlobalActor {
5 | public static let shared = RegexActor()
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/RegexTestSuite protocol.swift:
--------------------------------------------------------------------------------
1 | @testable import NativeRegexExamples
2 |
3 | /// A protocol used to ensure that each `@Suite` is testing for the same things.
4 | ///
5 | /// `RegexTestSuite` also adds `@RegexActor` so that you don't need to add it to tests or suites.
6 | @RegexActor
7 | public protocol RegexTestSuite {
8 | /// Use this test to prove that the input string WILL whole match the input string.
9 | ///
10 | ///
11 | func wholeMatch(_ input: String) async throws
12 | /// Use this test to prove that the input string WILL NOT whole match the input string.
13 | func not_wholeMatch(_ input: String) async throws
14 | func replace() async
15 | func falsePositives(_ input: String) throws
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/NativeRegexExamples/name spaces.swift:
--------------------------------------------------------------------------------
1 | /// A namespace to hold `Regex`s defined using the literal syntax
2 | @RegexActor
3 | public enum RegexLiterals {}
4 |
5 |
6 | /// A namespace to hold `Regex`s defined using the RegexBuilder syntax
7 | @RegexActor
8 | public enum RegexBuilders {}
9 |
10 | @RegexActor
11 | public enum RegexCustomParsers {}
12 |
13 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/DateTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import NativeRegexExamples
3 | import CustomDump
4 | import Foundation
5 | //
6 | @Suite(.tags(.builderDSL, .date))
7 | struct DateTests_MM_DD_YYYY_Literal: RegexTestSuite {
8 | @Test(arguments: ["01/01/1970"])
9 | func wholeMatch(_ input: String) throws {
10 | let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.date_MM_DD_YYYY)
11 | guard let wholeMatch = wholeMatchOptional else {
12 | Issue.record(); return
13 | }
14 | let output = wholeMatch.output // convert Substring to String
15 | let outputString = "\(output.1)/\(output.2)/\(output.3)"
16 | expectNoDifference(outputString, input)
17 | }
18 |
19 | @Test("NOT wholeMatch(of:)",
20 | arguments: ["Jan 1, 1970", "1/1/1970", "1970-01-01"]
21 | )
22 | func not_wholeMatch(_ input: String) throws {
23 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.date_MM_DD_YYYY)
24 | #expect(
25 | not_wholeMatch == nil,
26 | "False positive match found: \(input) should not match \(not_wholeMatch)"
27 | )
28 | }
29 |
30 | @Test("replace(_ regex: with:)")
31 | func replace() {
32 | var text = """
33 | 06/29/2007 some other text
34 | some other text 01/01/2000
35 | """
36 | text.replace(RegexLiterals.date_MM_DD_YYYY, with: "⬛︎⬛︎⬛︎")
37 | let expected = """
38 | ⬛︎⬛︎⬛︎ some other text
39 | some other text ⬛︎⬛︎⬛︎
40 | """
41 | expectNoDifference(expected, text)
42 | }
43 |
44 | @Test(arguments: [
45 | "02/29/2023", "04/31/2023" // parser doesn't account for overloaded dates
46 | ])
47 | func falsePositives(_ input: String) {
48 | withKnownIssue("False positive match found: \(input)") {
49 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.date_MM_DD_YYYY)
50 | #expect(not_wholeMatch == nil)
51 | }
52 | }
53 | }
54 |
55 |
56 | //@Suite(.tags(.builderDSL, .date))
57 | //struct DateTests_DSL: RegexTestSuite {
58 | // @Test(
59 | // arguments: [
60 | // "01/01/1970",
61 | // "Jan 1, 1970",
62 | // "January 1, 1970",
63 | // "Thursday, January 1, 1970"
64 | // ]
65 | // )
66 | // func wholeMatch(_ input: String) throws {
67 | // let wholeMatchOptional = input.wholeMatch(of: RegexBuilders.date_MM_DD_YYYY)
68 | // guard let wholeMatch = wholeMatchOptional else {
69 | // Issue.record(); return
70 | // }
71 | // wholeMatch.output
72 | // let date = Date(timeIntervalSince1970: 0)
73 | // expectNoDifference(date, output)
74 | // }
75 | //
76 | // @Test("NOT wholeMatch(of:)",
77 | // arguments: ["June 29, 2007", "02/29/2023", "12/32/2024", "1970-01-01"]
78 | // )
79 | // func not_wholeMatch(_ input: String) throws {
80 | // let not_wholeMatch = input.wholeMatch(of: RegexBuilders.date_MM_DD_YYYY)
81 | // #expect(
82 | // not_wholeMatch == nil,
83 | // "False positive match found: \(input) should not match \(not_wholeMatch)"
84 | // )
85 | // }
86 | //
87 | // @Test("replace(_ regex: with:)")
88 | // func replace() {
89 | // var text = """
90 | //06/29/2007 some other text
91 | //some other text 01/01/2000
92 | //"""
93 | // text.replace(RegexBuilders.date_MM_DD_YYYY, with: "⬛︎⬛︎⬛︎")
94 | // let expected = """
95 | //⬛︎⬛︎⬛︎ some other text
96 | //some other text ⬛︎⬛︎⬛︎
97 | //"""
98 | // expectNoDifference(expected, text)
99 | // }
100 | //}
101 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/EmailTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import NativeRegexExamples
3 | import CustomDump
4 |
5 | @Suite(.tags(.literals, .email))
6 | struct EmailTests_Literal: RegexTestSuite {
7 | @Test(arguments: ["hello@email.com", "myemail@something.co.uk", "user@sub.example.com", "some.name@place.com", "user..name@example.com"])
8 | func wholeMatch(_ input: String) throws {
9 | let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.email)
10 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
11 | let output = String(wholeMatch.output) // convert Substring to String
12 | expectNoDifference(output, input)
13 | }
14 |
15 | @Test("NOT wholeMatch(of:)",
16 | arguments: ["@email.com", "myName@"]
17 | )
18 | func not_wholeMatch(_ input: String) throws {
19 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.email)
20 | #expect(
21 | not_wholeMatch == nil,
22 | "False positive match found: \(input) should not match \(not_wholeMatch)"
23 | )
24 | }
25 |
26 | @Test("replace(_ regex: with:)")
27 | func replace() {
28 | var text = """
29 | hello@email.com some other text
30 | some other text myemail@example.org
31 | """
32 | text.replace(RegexLiterals.email, with: "⬛︎⬛︎⬛︎")
33 | let expected = """
34 | ⬛︎⬛︎⬛︎ some other text
35 | some other text ⬛︎⬛︎⬛︎
36 | """
37 | expectNoDifference(expected, text)
38 | }
39 |
40 | @Test(arguments: [String]())
41 | func falsePositives(_ input: String) {
42 | withKnownIssue("False positive match found: \(input)") {
43 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.email)
44 | #expect(not_wholeMatch == nil)
45 | }
46 | }
47 | }
48 |
49 | @Suite(.tags(.builderDSL, .email))
50 | struct EmailTests_DSL: RegexTestSuite {
51 | @Test(arguments: ["hello@email.com", "myemail@something.co.uk", "user@sub.example.com", "some.name@place.com", "user..name@example.com"])
52 | func wholeMatch(_ input: String) throws {
53 | let wholeMatchOptional = input.wholeMatch(of: RegexBuilders.email)
54 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
55 | let output = String(wholeMatch.output) // convert Substring to String
56 | expectNoDifference(output, input)
57 | }
58 |
59 | @Test("NOT wholeMatch(of:)",
60 | arguments: ["@email.com", "myName@"]
61 | )
62 | func not_wholeMatch(_ input: String) throws {
63 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.email)
64 | #expect(
65 | not_wholeMatch == nil,
66 | "False positive match found: \(input) should not match \(not_wholeMatch)"
67 | )
68 | }
69 |
70 | @Test("replace(_ regex: with:)")
71 | func replace() {
72 | var text = """
73 | hello@email.com some other text
74 | some other text myemail@example.org
75 | """
76 | text.replace(RegexBuilders.email, with: "⬛︎⬛︎⬛︎")
77 | let expected = """
78 | ⬛︎⬛︎⬛︎ some other text
79 | some other text ⬛︎⬛︎⬛︎
80 | """
81 | expectNoDifference(expected, text)
82 | }
83 |
84 | @Test(arguments: [String]())
85 | func falsePositives(_ input: String) {
86 | withKnownIssue("False positive match found: \(input)") {
87 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.email)
88 | #expect(not_wholeMatch == nil)
89 | }
90 | }
91 | }
92 |
93 |
94 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/IPv4Tests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import NativeRegexExamples
3 | import CustomDump
4 |
5 | @Suite(.tags(.literals, .ipv4))
6 | struct IPv4Tests_Literal: RegexTestSuite {
7 | @Test(arguments: [
8 | "127.0.0.1", "192.168.1.1", "0.0.0.0", "255.255.255.255", "1.2.3.4"
9 | ])
10 | func wholeMatch(_ input: String) throws {
11 | let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.ipv4)
12 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
13 | let output = String(wholeMatch.output) // convert Substring to String
14 | expectNoDifference(output, input)
15 | }
16 |
17 | @Test("NOT wholeMatch(of:)",
18 | arguments: ["256.256.256.256", "999.999.999.999", "1.2.3"]
19 | )
20 | func not_wholeMatch(_ input: String) throws {
21 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ipv4)
22 | #expect(
23 | not_wholeMatch == nil,
24 | "False positive match found: \(input) should not match \(not_wholeMatch)"
25 | )
26 | }
27 |
28 | @Test("replace(_ regex: with:)")
29 | func replace() {
30 | var text = """
31 | 192.168.1.1 some other text
32 | some other text 127.0.0.1
33 | """
34 | text.replace(RegexLiterals.ipv4, with: "⬛︎⬛︎⬛︎")
35 | let expected = """
36 | ⬛︎⬛︎⬛︎ some other text
37 | some other text ⬛︎⬛︎⬛︎
38 | """
39 | expectNoDifference(expected, text)
40 | }
41 |
42 | @Test(arguments: [String]())
43 | func falsePositives(_ input: String) {
44 | withKnownIssue("False positive match found: \(input)") {
45 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ipv4)
46 | #expect(not_wholeMatch == nil)
47 | }
48 | }
49 | }
50 |
51 | @Suite(.tags(.builderDSL, .ipv4))
52 | struct IPv4Tests_DSL: RegexTestSuite {
53 | @Test(arguments: [
54 | "127.0.0.1", "192.168.1.1", "0.0.0.0", "255.255.255.255", "1.2.3.4"
55 | ])
56 | func wholeMatch(_ input: String) throws {
57 | let wholeMatchOptional = input.wholeMatch(of: RegexBuilders.ipv4)
58 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
59 | let output = String(wholeMatch.output) // convert Substring to String
60 | expectNoDifference(output, input)
61 | }
62 |
63 | @Test("NOT wholeMatch(of:)",
64 | arguments: ["256.256.256.256", "999.999.999.999", "1.2.3"]
65 | )
66 | func not_wholeMatch(_ input: String) throws {
67 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.ipv4)
68 | #expect(
69 | not_wholeMatch == nil,
70 | "False positive match found: \(input) should not match \(not_wholeMatch)"
71 | )
72 | }
73 |
74 | @Test("replace(_ regex: with:)")
75 | func replace() {
76 | var text = """
77 | 192.168.1.1 some other text
78 | some other text 127.0.0.1
79 | """
80 | text.replace(RegexBuilders.ipv4, with: "⬛︎⬛︎⬛︎")
81 | let expected = """
82 | ⬛︎⬛︎⬛︎ some other text
83 | some other text ⬛︎⬛︎⬛︎
84 | """
85 | expectNoDifference(expected, text)
86 | }
87 |
88 | @Test(arguments: [String]())
89 | func falsePositives(_ input: String) {
90 | withKnownIssue("False positive match found: \(input)") {
91 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.ipv4)
92 | #expect(not_wholeMatch == nil)
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/PhoneNumbers/Experimental/PhoneNumberDataDetectorTests.swift:
--------------------------------------------------------------------------------
1 | import CustomDump
2 | @_spi(Experimental) import NativeRegexExamples
3 | import Testing
4 |
5 | // MARK: PhoneNumberDataDetector
6 | @Suite(.tags(.customParsers, .phoneNumber), .disabled())
7 | struct PhoneNumberDataDetectorTests: RegexTestSuite {
8 | @Test(arguments: ["555-1234", "1-808-555-1234", "(808) 555-1234", "555-12345", "1 (808) 555-1234", "5555-1234", "55-1234", "5551234", "1-55-1234"])
9 | func wholeMatch(_ input: String) throws {
10 | let wholeMatchOptional = input.wholeMatch(of: RegexCustomParsers.phoneNumberDataDetector)
11 | guard let wholeMatch = wholeMatchOptional else {
12 | Issue.record("whole match for input: \(input) not found")
13 | return
14 | }
15 | let output = String(wholeMatch.output)
16 | expectNoDifference(output, input)
17 | }
18 |
19 | @Test("NOT wholeMatch(of:)",
20 | arguments: ["555-12345", "55-1234", "5551234"]
21 | )
22 | func not_wholeMatch(_ input: String) throws {
23 | let not_wholeMatch = input.wholeMatch(of: RegexCustomParsers.phoneNumberDataDetector)
24 | withKnownIssue {
25 | #expect(
26 | not_wholeMatch == nil,
27 | "False positive match found: \(input) should not match \(String(not_wholeMatch?.output ?? ""))"
28 | )
29 | }
30 | }
31 |
32 | @Test
33 | func replace() {
34 | var text = """
35 | 555-1234 some other text
36 | some other text 555-1234
37 | """
38 | text.replace(RegexCustomParsers.phoneNumberDataDetector, with: "⬛︎⬛︎⬛︎")
39 | let expected = """
40 | ⬛︎⬛︎⬛︎ some other text
41 | some other text ⬛︎⬛︎⬛︎
42 | """
43 | expectNoDifference(expected, text)
44 | }
45 |
46 | @Test(arguments: [String]())
47 | func falsePositives(_ input: String) {
48 | withKnownIssue("False positive match found: \(input)") {
49 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.phoneNumber)
50 | #expect(not_wholeMatch == nil)
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/PhoneNumbers/Experimental/PhoneNumberKitParser.swift:
--------------------------------------------------------------------------------
1 | import CustomDump
2 | @_spi(Experimental) import NativeRegexExamples
3 | import Testing
4 |
5 |
6 | // MARK: PhoneNumberKit
7 | @Suite(.tags(.customParsers, .phoneNumber), .disabled())
8 | struct PhoneNumberKitCustomRegexComponentTests: RegexTestSuite {
9 | @Test(arguments: ["(555) 555-1234", "555-1234", "5555-1234", "55-1234", "5551234", "1-555-1234"])
10 | func wholeMatch(_ input: String) throws {
11 | let wholeMatchOptional = input.wholeMatch(of: RegexCustomParsers.phoneNumberKit)
12 | guard let wholeMatch = wholeMatchOptional else {
13 | Issue.record("whole match for input: \(input) not found")
14 | return
15 | }
16 | let output = String(wholeMatch.output)
17 | expectNoDifference(output, input)
18 | }
19 |
20 | @Test("NOT wholeMatch(of:)",
21 | arguments: ["5555-1234", "555-12345", "55-1234", "5551234", "1-55-1234"]
22 | )
23 | func not_wholeMatch(_ input: String) throws {
24 | let not_wholeMatch = input.wholeMatch(of: RegexCustomParsers.phoneNumberDataDetector)
25 | withKnownIssue {
26 | Issue.record("input: \(input), not_wholeMatch: \(String(describingForTest: not_wholeMatch))")
27 | #expect(
28 | not_wholeMatch == nil,
29 | "False positive match found: \(input) should not match \(String(not_wholeMatch?.output ?? ""))"
30 | )
31 | }
32 | }
33 |
34 | @Test
35 | func replace() {
36 | var text = """
37 | 555-1234 some other text
38 | some other text 555-1234
39 | """
40 | text.replace(RegexCustomParsers.phoneNumberDataDetector, with: "⬛︎⬛︎⬛︎")
41 | let expected = """
42 | ⬛︎⬛︎⬛︎ some other text
43 | some other text ⬛︎⬛︎⬛︎
44 | """
45 | expectNoDifference(expected, text)
46 | }
47 |
48 | @Test(arguments: [String]())
49 | func falsePositives(_ input: String) {
50 | withKnownIssue("False positive match found: \(input)") {
51 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.phoneNumber)
52 | #expect(not_wholeMatch == nil)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/PhoneNumbers/PhoneNumberTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import NativeRegexExamples
3 | import CustomDump
4 |
5 | extension Tag {
6 | // MARK: Regex Implementation Types
7 | /// A Regex component implemented using a Regex literals.
8 | @Tag static var literals: Self
9 | /// A Regex component implemented using the `RegexBuilder` DSL.
10 | @Tag static var builderDSL: Self
11 | /// A Regex component implemented using `CustomConsumingRegexComponent`.
12 | @Tag static var customParsers: Self
13 |
14 | // MARK: Data Types
15 | @Tag static var phoneNumber: Self
16 | @Tag static var email: Self
17 | @Tag static var date: Self
18 | @Tag static var ipv4: Self
19 | @Tag static var ssn: Self
20 | }
21 |
22 | @Suite(.tags(.literals, .phoneNumber))
23 | struct Literals_PhoneNumberTests: RegexTestSuite {
24 | @Test(arguments: ["555-1234", "5551234", "1-555-1234"])
25 | func wholeMatch(_ input: String) throws {
26 | let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.phoneNumber)
27 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
28 | let output = String(wholeMatch.output) // convert Substring to String
29 | expectNoDifference(output, input)
30 | }
31 |
32 | @Test("NOT wholeMatch(of:)",
33 | arguments: ["5555-1234", "55-1234", "555-12345"]
34 | )
35 | func not_wholeMatch(_ input: String) throws {
36 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.phoneNumber)
37 | withKnownIssue {
38 | #expect(
39 | not_wholeMatch == nil,
40 | "False positive match found: \(input) should not match \(String(not_wholeMatch?.output ?? ""))"
41 | )
42 | }
43 | }
44 |
45 | @Test("NOT passing in a Regex through @Test")
46 | func replace() {
47 | var text = """
48 | 555-1234 some other text
49 | some other text 1-555-1234
50 | """
51 | text.replace(RegexLiterals.phoneNumber, with: "⬛︎⬛︎⬛︎")
52 | let expected = """
53 | ⬛︎⬛︎⬛︎ some other text
54 | some other text ⬛︎⬛︎⬛︎
55 | """
56 | expectNoDifference(expected, text)
57 | }
58 |
59 | @Test(arguments: [String]())
60 | func falsePositives(_ input: String) {
61 | withKnownIssue("False positive match found: \(input)") {
62 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.phoneNumber)
63 | #expect(not_wholeMatch == nil)
64 | }
65 | }
66 | }
67 |
68 | @Suite(.tags(.builderDSL, .phoneNumber))
69 | struct Builder_PhoneNumberTests: RegexTestSuite {
70 | @Test(arguments: ["555-1234", "5551234", "1-555-1234"])
71 | func wholeMatch(_ input: String) throws {
72 | let wholeMatchOptional = input.wholeMatch(of: RegexBuilders.phoneNumber)
73 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
74 | let output = String(wholeMatch.output) // convert Substring to String
75 | expectNoDifference(output, input)
76 | }
77 |
78 | @Test("NOT wholeMatch(of:)",
79 | arguments: ["5555-1234", "55-1234", "555-12345"]
80 | )
81 | func not_wholeMatch(_ input: String) throws {
82 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.phoneNumber)
83 | withKnownIssue {
84 | #expect(
85 | not_wholeMatch == nil,
86 | "False positive match found: \(input) should not match \(String(not_wholeMatch?.output ?? ""))"
87 | )
88 | }
89 | }
90 |
91 | @Test
92 | func replace() {
93 | var text = """
94 | 555-1234 some other text
95 | some other text 1-555-1234
96 | """
97 | text.replace(RegexBuilders.phoneNumber, with: "⬛︎⬛︎⬛︎")
98 | let expected = """
99 | ⬛︎⬛︎⬛︎ some other text
100 | some other text ⬛︎⬛︎⬛︎
101 | """
102 | expectNoDifference(expected, text)
103 | }
104 |
105 |
106 | @Test(arguments: [String]())
107 | func falsePositives(_ input: String) {
108 | withKnownIssue("False positive match found: \(input)") {
109 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.phoneNumber)
110 | #expect(not_wholeMatch == nil)
111 | }
112 | }
113 | }
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Tests/NativeRegexExamplesTests/SSNTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import NativeRegexExamples
3 | import CustomDump
4 |
5 | @Suite(.tags(.literals, .ssn))
6 | struct SSNTests_Literal: RegexTestSuite {
7 | @Test(arguments: ["123-45-6789"])
8 | func wholeMatch(_ input: String) throws {
9 | let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.ssn)
10 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
11 | let output = String(wholeMatch.output) // convert Substring to String
12 | expectNoDifference(output, input)
13 | }
14 |
15 | @Test(
16 | "NOT wholeMatch(of:)",
17 | arguments: [
18 | "some other text", "", "-11-1111",
19 | "666-11-1111", "000-11-1111", "900-11-1111"
20 | ]
21 | )
22 | func not_wholeMatch(_ input: String) throws {
23 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ssn)
24 | #expect(
25 | not_wholeMatch == nil,
26 | "False positive match found: \(input) should not match \(not_wholeMatch)"
27 | )
28 | }
29 |
30 | @Test("replace(_ regex: with:)")
31 | func replace() {
32 | var text = """
33 | 111-11-1111 some other text
34 | some other text 222-22-2222
35 | """
36 | text.replace(RegexLiterals.ssn, with: "⬛︎⬛︎⬛︎")
37 | let expected = """
38 | ⬛︎⬛︎⬛︎ some other text
39 | some other text ⬛︎⬛︎⬛︎
40 | """
41 | expectNoDifference(expected, text)
42 | }
43 |
44 | @Test(arguments: [String]())
45 | func falsePositives(_ input: String) {
46 | withKnownIssue("False positive match found: \(input)") {
47 | let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ssn)
48 | #expect(not_wholeMatch == nil)
49 | }
50 | }
51 | }
52 |
53 | @Suite(.tags(.builderDSL, .ssn))
54 | struct SSNTests_DSL: RegexTestSuite {
55 | @Test(arguments: ["123-45-6789"])
56 | func wholeMatch(_ input: String) throws {
57 | let wholeMatchOptional = input.wholeMatch(of: RegexBuilders.ssn)
58 | let wholeMatch = try #require(wholeMatchOptional) // unwrap
59 | let output = String(wholeMatch.output) // convert Substring to String
60 | expectNoDifference(output, input)
61 | }
62 |
63 | @Test(
64 | "NOT wholeMatch(of:)",
65 | arguments: [
66 | "some other text", "", "-11-1111",
67 | "666-11-1111", "000-11-1111", "900-11-1111"
68 | ]
69 | )
70 | func not_wholeMatch(_ input: String) throws {
71 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.ssn)
72 | #expect(
73 | not_wholeMatch == nil,
74 | "False positive match found: \(input) should not match \(not_wholeMatch)"
75 | )
76 | }
77 |
78 | @Test("replace(_ regex: with:)")
79 | func replace() {
80 | var text = """
81 | 111-11-1111 some other text
82 | some other text 222-22-2222
83 | """
84 | text.replace(RegexBuilders.ssn, with: "⬛︎⬛︎⬛︎")
85 | let expected = """
86 | ⬛︎⬛︎⬛︎ some other text
87 | some other text ⬛︎⬛︎⬛︎
88 | """
89 | expectNoDifference(expected, text)
90 | }
91 |
92 | @Test(arguments: [String]())
93 | func falsePositives(_ input: String) {
94 | withKnownIssue("False positive match found: \(input)") {
95 | let not_wholeMatch = input.wholeMatch(of: RegexBuilders.ssn)
96 | #expect(not_wholeMatch == nil)
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------