├── .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://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FDandyLyons%2FNativeRegexExamples%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/DandyLyons/NativeRegexExamples) 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FDandyLyons%2FNativeRegexExamples%2Fbadge%3Ftype%3Dplatforms)](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 | Star History Chart 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 | ![Confusion Matrix](https://upload.wikimedia.org/wikipedia/commons/3/32/Binary_confusion_matrix.jpg) 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 | --------------------------------------------------------------------------------