diff --git a/.swiftpm/configuration/Package.resolved b/.swiftpm/configuration/Package.resolved
new file mode 100644
index 0000000..ef713d1
--- /dev/null
+++ b/.swiftpm/configuration/Package.resolved
@@ -0,0 +1,24 @@
+{
+ "originHash" : "3abce946d7d39337100fed5938f428ba7e161c6175e38ec9e84d5c553dcc8467",
+ "pins" : [
+ {
+ "identity" : "swift-custom-dump",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
+ "state" : {
+ "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
+ "version" : "1.3.3"
+ }
+ },
+ {
+ "identity" : "xctest-dynamic-overlay",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
+ "state" : {
+ "revision" : "96beb108a57f24c8476ae1f309239270772b2940",
+ "version" : "1.2.5"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NativeRegexExamples.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NativeRegexExamples.xcscheme
new file mode 100644
index 0000000..5040072
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/NativeRegexExamples.xcscheme
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.swift b/Package.swift
index 8c95bf0..285c52e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,10 +1,11 @@
-// swift-tools-version: 6.0
+// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "NativeRegexExamples",
+ platforms: [.iOS(.v16), .macOS(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
@@ -15,10 +16,22 @@ let package = Package(
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
- name: "NativeRegexExamples"),
+ name: "NativeRegexExamples",
+ dependencies: [
+ .product(name: "CustomDump", package: "swift-custom-dump"),
+ ],
+ swiftSettings: [
+ .enableUpcomingFeature("BareSlashRegexLiterals"),
+ .enableExperimentalFeature("StrictConcurrency"),
+ ]
+ ),
.testTarget(
name: "NativeRegexExamplesTests",
- dependencies: ["NativeRegexExamples"]
+ dependencies: [
+ "NativeRegexExamples",
+ .product(name: "CustomDump", package: "swift-custom-dump"),
+ ]
),
- ]
+ ],
+ swiftLanguageVersions: [.v5]
)
diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift
new file mode 100644
index 0000000..057452b
--- /dev/null
+++ b/Package@swift-6.0.swift
@@ -0,0 +1,36 @@
+// swift-tools-version: 6.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "NativeRegexExamples",
+ platforms: [.iOS(.v16), .macOS(.v13)],
+ products: [
+ // Products define the executables and libraries a package produces, making them visible to other packages.
+ .library(
+ name: "NativeRegexExamples",
+ targets: ["NativeRegexExamples"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3"), // Custom Dump
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
+ // Targets can depend on other targets in this package and products from dependencies.
+ .target(
+ name: "NativeRegexExamples",
+ dependencies: [
+ .product(name: "CustomDump", package: "swift-custom-dump"),
+ ]
+ ),
+ .testTarget(
+ name: "NativeRegexExamplesTests",
+ dependencies: [
+ "NativeRegexExamples",
+ .product(name: "CustomDump", package: "swift-custom-dump"),
+ ]
+ ),
+ ],
+ swiftLanguageModes: [.v6]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1939dc7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,161 @@
+# NativeRegexExamples
+NativeRegexExamples is a place for the Swift community to:
+1. crowd-source `Regex` solutions that can be used in your projects
+2. learn from each other and develop best practices
+ - Provide cheat sheets
+3. test Regexes for:
+ - matches: so we can assess their capabilities
+ - non-matches: so we can eliminate false positives
+ - replacing capabilities
+
+## Basic Usage
+```swift
+@RegexActor
+func foo() {
+ let ssnRegex = RegexLiterals.ssn
+ let string = "111-11-1111"
+ string.contains(ssnRegex) // true
+ string.wholeMatch(of: ssnRegex)
+
+ var text = """
+one SSN -> 111-11-1111
+222-22-2222 <- another SSN
+"""
+ text.replace(ssnRegex, with: "___")
+// text is now:
+// one SSN -> ___
+// ⬛︎⬛︎⬛︎ <- another SSN
+}
+```
+
+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:
+```swift
+public extension RegexLiterals {
+ static let ssn = #/
+ # Area number: Can't be 000-199 or 666
+ (?!0{3})(?!6{3})[0-8]\d{2}
+ -
+ # Group number: Can't be 00
+ (?!0{2})\d{2}
+ -
+ # Serial number: Can't be 0000
+ (?!0{4})\d{4}
+ /#
+}
+
+public extension RegexBuilders {
+ static let ssn = Regex {
+ NegativeLookahead {
+ Repeat(count: 3) {
+ "0"
+ }
+ }
+ NegativeLookahead {
+ Repeat(count: 3) {
+ "6"
+ }
+ }
+ ("0"..."8")
+ Repeat(count: 2) {
+ One(.digit)
+ }
+ "-"
+ NegativeLookahead {
+ Repeat(count: 2) {
+ "0"
+ }
+ }
+ Repeat(count: 2) {
+ One(.digit)
+ }
+ "-"
+ NegativeLookahead {
+ Repeat(count: 4) {
+ "0"
+ }
+ }
+ Repeat(count: 4) {
+ One(.digit)
+ }
+ }
+ .anchorsMatchLineEndings()
+}
+```
+
+## Motivation
+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:
+
+1. Strict compile time type-checking
+2. Syntax highlighting for Regex literals
+3. An optional, more readable, DSL through RegexBuilder
+
+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.
+
+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!
+
+## Contributing
+Contributions are greatly appreciated for the benefit of the Swift community. Please feel free to file a PR or Issue!
+
+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.
+
+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.
+
+### Recommended Resources
+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.
+
+## Inspirations
+- [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
+- [iHateRegex.com](https://ihateregex.io/playground) can visualize regular expression logic.
+
+## Gotchas
+### Strict Concurrency Checking
+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`.
+
+## Recommended Resources
+
+### Swift Regex
+- [WWDC22 Meet Swift Regex](https://developer.apple.com/videos/play/wwdc2022/110357/)
+- [WWDC22 Swift Regex: Beyond the basics](https://developer.apple.com/videos/play/wwdc2022/110358)
+
+### Swift Testing
+- Video series: [Swift and Tips | Mastering Swift Testing series](https://www.youtube.com/watch?v=zXjM1cFUwW4&list=PLHWvYoDHvsOV67md_mU5nMN_HDZK7rEKn&pp=iAQB)
+- [Mastering the Swift Testing Framework | Fatbobman's Blog](https://fatbobman.com/en/posts/mastering-the-swift-testing-framework/#parameterized-testing)
+- 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.
+
+%% ## Installation
+Add NativeRegexExamples as a package dependency in your project's Package.swift:
+
+```swift
+// swift-tools-version:6.0
+import PackageDescription
+
+let package = Package(
+ name: "MyPackage",
+ dependencies: [
+ .package(
+ url: "https://github.com/ladvoc/BijectiveDictionary.git",
+ .upToNextMinor(from: "0.1.0")
+ )
+ ],
+ targets: [
+ .target(
+ name: "MyTarget",
+ dependencies: [
+ .product(name: "BijectiveDictionary", package: "BijectiveDictionary")
+ ]
+ )
+ ]
+)
+```
+%%
+
+## Project Status
+The project is in an early development phase. Current goals:
+
+- [ ] **More examples with passing tests**: Increase examples to all common use cases of regular expressions
+- [ ] **Documentation**: Ensure accuracy and completeness of documentation and include code examples.
+
+Your contributions are very welcome!
+
+## License
+This project is licensed under the MIT License. See the [LICENSE file](https://github.com/ladvoc/BijectiveDictionary/blob/main/LICENSE) for details.
diff --git a/Sources/NativeRegexExamples/DataTypes/Date.swift b/Sources/NativeRegexExamples/DataTypes/Date.swift
new file mode 100644
index 0000000..bba603a
--- /dev/null
+++ b/Sources/NativeRegexExamples/DataTypes/Date.swift
@@ -0,0 +1,57 @@
+import RegexBuilder
+
+public extension RegexLiterals {
+ /// A Regex literal which parses dates in the MM/DD/YYYY format.
+ ///
+ /// This regex is provided for learning purposes, but it is much better to use the regex parsers that ship in the
+ /// Foundation library as they support a wide variety of formats and they account for locale.
+ static let date_MM_DD_YYYY = #/\b(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])/(\d{4})\b/#
+}
+
+public extension RegexBuilders {
+ /// A Regex literal which parses dates in the MM/DD/YYYY format.
+ ///
+ /// This regex is provided for learning purposes, but it is much better to use the regex parsers that ship in the
+ /// Foundation library as they support a wide variety of formats and they account for locale.
+ static let date_MM_DD_YYYY: Regex<(Substring, Substring, Substring, Substring)> = Regex {
+ Anchor.wordBoundary
+ Capture {
+ ChoiceOf {
+ Regex {
+ "0"
+ ("1"..."9")
+ }
+ Regex {
+ "1"
+ ("0"..."2")
+ }
+ }
+ }
+ "/"
+ Capture {
+ ChoiceOf {
+ Regex {
+ "0"
+ ("1"..."9")
+ }
+ Regex {
+ One(.anyOf("12"))
+ ("0"..."9")
+ }
+ Regex {
+ "3"
+ One(.anyOf("01"))
+ }
+ }
+ }
+ "/"
+ Capture {
+ Repeat(count: 4) {
+ One(.digit)
+ }
+ }
+ Anchor.wordBoundary
+ }
+ .anchorsMatchLineEndings()
+
+}
diff --git a/Sources/NativeRegexExamples/DataTypes/IPv4.swift b/Sources/NativeRegexExamples/DataTypes/IPv4.swift
new file mode 100644
index 0000000..3a75fd8
--- /dev/null
+++ b/Sources/NativeRegexExamples/DataTypes/IPv4.swift
@@ -0,0 +1,55 @@
+import RegexBuilder
+
+public extension RegexLiterals {
+ static let ipv4: Regex = #/
+ (?:\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}
+ /#
+}
+
+
+public extension RegexBuilders {
+ static let ipv4 = Regex {
+ ChoiceOf {
+ Regex {
+ Anchor.wordBoundary
+ "25"
+ ("0"..."5")
+ }
+ Regex {
+ Anchor.wordBoundary
+ "2"
+ ("0"..."4")
+ ("0"..."9")
+ }
+ Regex {
+ Anchor.wordBoundary
+ Optionally(.anyOf("01"))
+ ("0"..."9")
+ Optionally(("0"..."9"))
+ }
+ }
+ Repeat(count: 3) {
+ Regex {
+ "."
+ ChoiceOf {
+ Regex {
+ "25"
+ ("0"..."5")
+ }
+ Regex {
+ "2"
+ ("0"..."4")
+ ("0"..."9")
+ }
+ Regex {
+ Optionally(.anyOf("01"))
+ ("0"..."9")
+ Optionally(("0"..."9"))
+ }
+ }
+ }
+ }
+ }
+ .anchorsMatchLineEndings()
+
+}
diff --git a/Sources/NativeRegexExamples/DataTypes/Phone Numbers.swift b/Sources/NativeRegexExamples/DataTypes/Phone Numbers.swift
new file mode 100644
index 0000000..1dec902
--- /dev/null
+++ b/Sources/NativeRegexExamples/DataTypes/Phone Numbers.swift
@@ -0,0 +1,66 @@
+import RegexBuilder
+
+public extension RegexLiterals {
+ /// A regex that identifies phone numbers.
+ ///
+ /// Have a look at the source code for this regex. It's a great example of Swift's extended delimiter literal
+ /// syntax. In this syntaax, whitespace is ignored and comments can be added, meaning complex
+ /// regex syntax can be used by split up in a way that is far more readable.
+ static let phoneNumber = #/
+ (?:\+\d{1,3}\s?)? # Optional international code
+ (?:\d{1,4}[-.\s]?)? # Optional country or area code
+ \d{1,4}[-.\s]? # Area code or local part
+ \d{1,4}[-.\s]? # Local part or line number
+ \d{1,4} # Line number
+ /#
+}
+
+public extension RegexBuilders {
+ static let phoneNumber = Regex {
+ Optionally {
+ Regex {
+ "+"
+ Repeat(1...3) {
+ One(.digit)
+ }
+ Optionally(.whitespace)
+ }
+ }
+ Optionally {
+ Regex {
+ Repeat(1...4) {
+ One(.digit)
+ }
+ Optionally {
+ CharacterClass(
+ .anyOf("-."),
+ .whitespace
+ )
+ }
+ }
+ }
+ Repeat(1...4) {
+ One(.digit)
+ }
+ Optionally {
+ CharacterClass(
+ .anyOf("-."),
+ .whitespace
+ )
+ }
+ Repeat(1...4) {
+ One(.digit)
+ }
+ Optionally {
+ CharacterClass(
+ .anyOf("-."),
+ .whitespace
+ )
+ }
+ Repeat(1...4) {
+ One(.digit)
+ }
+ }
+ .anchorsMatchLineEndings()
+
+}
diff --git a/Sources/NativeRegexExamples/DataTypes/SSN.swift b/Sources/NativeRegexExamples/DataTypes/SSN.swift
new file mode 100644
index 0000000..dac3856
--- /dev/null
+++ b/Sources/NativeRegexExamples/DataTypes/SSN.swift
@@ -0,0 +1,53 @@
+import RegexBuilder
+
+public extension RegexLiterals {
+ static let ssn = #/
+ # Area number: Can't be 000-199 or 666
+ (?!0{3})(?!6{3})[0-8]\d{2}
+ -
+ # Group number: Can't be 00
+ (?!0{2})\d{2}
+ -
+ # Serial number: Can't be 0000
+ (?!0{4})\d{4}
+ /#
+}
+
+public extension RegexBuilders {
+ static let ssn = Regex {
+ NegativeLookahead {
+ Repeat(count: 3) {
+ "0"
+ }
+ }
+ NegativeLookahead {
+ Repeat(count: 3) {
+ "6"
+ }
+ }
+ ("0"..."8")
+ Repeat(count: 2) {
+ One(.digit)
+ }
+ "-"
+ NegativeLookahead {
+ Repeat(count: 2) {
+ "0"
+ }
+ }
+ Repeat(count: 2) {
+ One(.digit)
+ }
+ "-"
+ NegativeLookahead {
+ Repeat(count: 4) {
+ "0"
+ }
+ }
+ Repeat(count: 4) {
+ One(.digit)
+ }
+ }
+ .anchorsMatchLineEndings()
+
+}
diff --git a/Sources/NativeRegexExamples/DataTypes/email.swift b/Sources/NativeRegexExamples/DataTypes/email.swift
new file mode 100644
index 0000000..0bb856d
--- /dev/null
+++ b/Sources/NativeRegexExamples/DataTypes/email.swift
@@ -0,0 +1,37 @@
+import RegexBuilder
+
+public extension RegexLiterals {
+ static let email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/
+}
+
+public extension RegexBuilders {
+ static let email = Regex {
+ "/"
+ OneOrMore {
+ CharacterClass(
+ .anyOf("._%+-"),
+ ("A"..."Z"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ "@"
+ OneOrMore {
+ CharacterClass(
+ .anyOf(".-"),
+ ("A"..."Z"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ "."
+ Repeat(2...) {
+ CharacterClass(
+ ("A"..."Z"),
+ ("a"..."z")
+ )
+ }
+ "/"
+ }
+ .anchorsMatchLineEndings()
+}
diff --git a/Sources/NativeRegexExamples/Literals.swift b/Sources/NativeRegexExamples/Literals.swift
new file mode 100644
index 0000000..c39be46
--- /dev/null
+++ b/Sources/NativeRegexExamples/Literals.swift
@@ -0,0 +1,4 @@
+
+/// A namespace to hold `Regex`s defined using the literal syntax
+@RegexActor
+public enum RegexLiterals {}
diff --git a/Sources/NativeRegexExamples/NativeRegexExamples.swift b/Sources/NativeRegexExamples/NativeRegexExamples.swift
deleted file mode 100644
index 08b22b8..0000000
--- a/Sources/NativeRegexExamples/NativeRegexExamples.swift
+++ /dev/null
@@ -1,2 +0,0 @@
-// The Swift Programming Language
-// https://docs.swift.org/swift-book
diff --git a/Sources/NativeRegexExamples/RegexActor.swift b/Sources/NativeRegexExamples/RegexActor.swift
new file mode 100644
index 0000000..5e203de
--- /dev/null
+++ b/Sources/NativeRegexExamples/RegexActor.swift
@@ -0,0 +1,7 @@
+/// A global actor for isolating `Regex`es
+///
+/// Unfortunately, `Regex` is not `Sendable` which means we must isolate our library `Regex`s.
+@globalActor public actor RegexActor: GlobalActor {
+ public static let shared = RegexActor()
+}
+
diff --git a/Sources/NativeRegexExamples/RegexBuilders.swift b/Sources/NativeRegexExamples/RegexBuilders.swift
new file mode 100644
index 0000000..b0ff968
--- /dev/null
+++ b/Sources/NativeRegexExamples/RegexBuilders.swift
@@ -0,0 +1,6 @@
+/// A namespace to hold `Regex`s defined using the RegexBuilder syntax
+@RegexActor
+public enum RegexBuilders {}
+
+
+
diff --git a/Tests/NativeRegexExamplesTests/EmailTests.swift b/Tests/NativeRegexExamplesTests/EmailTests.swift
new file mode 100644
index 0000000..fc60dff
--- /dev/null
+++ b/Tests/NativeRegexExamplesTests/EmailTests.swift
@@ -0,0 +1,41 @@
+import Testing
+@testable import NativeRegexExamples
+import CustomDump
+
+@Suite("Email")
+struct EmailTests: RegexTestSuite {
+ @Test(arguments: ["hello@email.com", "myemail@something.co.uk", "user@sub.example.com", "some.name@place.com", "user..name@example.com"])
+ func wholeMatch(_ input: String) throws {
+ let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.email)
+ let wholeMatch = try #require(wholeMatchOptional) // unwrap
+ let output = String(wholeMatch.output) // convert Substring to String
+ expectNoDifference(output, input)
+ }
+
+ @Test("NOT wholeMatch(of:)",
+ arguments: ["@email.com", "myName@"]
+ )
+ func not_wholeMatch(_ input: String) throws {
+ let not_wholeMatch = input.wholeMatch(of: RegexLiterals.email)
+ #expect(
+ not_wholeMatch == nil,
+ "False positive match found: \(input) should not match \(not_wholeMatch)"
+ )
+ }
+
+ @Test("replace(_ regex: with:)")
+ func replace() {
+ var text = """
+hello@email.com some other text
+some other text myemail@example.org
+"""
+ text.replace(RegexLiterals.email, with: "⬛︎⬛︎⬛︎")
+ let expected = """
+⬛︎⬛︎⬛︎ some other text
+some other text ⬛︎⬛︎⬛︎
+"""
+ expectNoDifference(expected, text)
+ }
+}
+
+
diff --git a/Tests/NativeRegexExamplesTests/IPv4Tests.swift b/Tests/NativeRegexExamplesTests/IPv4Tests.swift
new file mode 100644
index 0000000..2e9a431
--- /dev/null
+++ b/Tests/NativeRegexExamplesTests/IPv4Tests.swift
@@ -0,0 +1,41 @@
+import Testing
+@testable import NativeRegexExamples
+import CustomDump
+
+@Suite("IPv4")
+struct IPv4Tests: RegexTestSuite {
+ @Test(arguments: [
+ "127.0.0.1", "192.168.1.1", "0.0.0.0", "255.255.255.255", "1.2.3.4"
+ ])
+ func wholeMatch(_ input: String) throws {
+ let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.ipv4)
+ let wholeMatch = try #require(wholeMatchOptional) // unwrap
+ let output = String(wholeMatch.output) // convert Substring to String
+ expectNoDifference(output, input)
+ }
+
+ @Test("NOT wholeMatch(of:)",
+ arguments: ["256.256.256.256", "999.999.999.999", "1.2.3"]
+ )
+ func not_wholeMatch(_ input: String) throws {
+ let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ipv4)
+ #expect(
+ not_wholeMatch == nil,
+ "False positive match found: \(input) should not match \(not_wholeMatch)"
+ )
+ }
+
+ @Test("replace(_ regex: with:)")
+ func replace() {
+ var text = """
+192.168.1.1 some other text
+some other text 127.0.0.1
+"""
+ text.replace(RegexLiterals.ipv4, with: "⬛︎⬛︎⬛︎")
+ let expected = """
+⬛︎⬛︎⬛︎ some other text
+some other text ⬛︎⬛︎⬛︎
+"""
+ expectNoDifference(expected, text)
+ }
+}
diff --git a/Tests/NativeRegexExamplesTests/NativeRegexExamplesTests.swift b/Tests/NativeRegexExamplesTests/NativeRegexExamplesTests.swift
deleted file mode 100644
index 8abdb2a..0000000
--- a/Tests/NativeRegexExamplesTests/NativeRegexExamplesTests.swift
+++ /dev/null
@@ -1,6 +0,0 @@
-import Testing
-@testable import NativeRegexExamples
-
-@Test func example() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
-}
diff --git a/Tests/NativeRegexExamplesTests/PhoneNumberTests.swift b/Tests/NativeRegexExamplesTests/PhoneNumberTests.swift
new file mode 100644
index 0000000..3d8b97d
--- /dev/null
+++ b/Tests/NativeRegexExamplesTests/PhoneNumberTests.swift
@@ -0,0 +1,42 @@
+import Testing
+@testable import NativeRegexExamples
+import CustomDump
+
+@Suite("Phone Numbers")
+struct PhoneNumberTests: RegexTestSuite {
+ @Test(arguments: ["555-1234", "5551234", "1-555-1234"])
+ func wholeMatch(_ input: String) throws {
+ let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.phoneNumber)
+ let wholeMatch = try #require(wholeMatchOptional) // unwrap
+ let output = String(wholeMatch.output) // convert Substring to String
+ expectNoDifference(output, input)
+ }
+
+ @Test("NOT wholeMatch(of:)",
+ arguments: ["5555-1234", "55-1234", "555-12345"]
+ )
+ func not_wholeMatch(_ input: String) throws {
+ let not_wholeMatch = input.wholeMatch(of: RegexLiterals.phoneNumber)
+ withKnownIssue {
+ #expect(
+ not_wholeMatch == nil,
+ "False positive match found: \(input) should not match \(String(not_wholeMatch?.output ?? ""))"
+ )
+ }
+ }
+
+ @Test("NOT passing in a Regex through @Test")
+ func replace() {
+ var text = """
+555-1234 some other text
+some other text 1-555-1234
+"""
+ text.replace(RegexLiterals.phoneNumber, with: "⬛︎⬛︎⬛︎")
+ let expected = """
+⬛︎⬛︎⬛︎ some other text
+some other text ⬛︎⬛︎⬛︎
+"""
+ expectNoDifference(expected, text)
+ }
+
+}
diff --git a/Tests/NativeRegexExamplesTests/RegexTestSuite protocol.swift b/Tests/NativeRegexExamplesTests/RegexTestSuite protocol.swift
new file mode 100644
index 0000000..929a7c5
--- /dev/null
+++ b/Tests/NativeRegexExamplesTests/RegexTestSuite protocol.swift
@@ -0,0 +1,12 @@
+@testable import NativeRegexExamples
+
+/// A protocol used to ensure that each `@TestSuite` is testing for the same things.
+///
+/// `RegexTestSuite` also adds `@RegexActor` so that you don't need to add it to tests or suites.
+@RegexActor
+protocol RegexTestSuite {
+ func wholeMatch(_ input: String) async throws
+ /// use this to prove that we are not matching false positives
+ func not_wholeMatch(_ input: String) async throws
+ func replace() async
+}
diff --git a/Tests/NativeRegexExamplesTests/SSNTests.swift b/Tests/NativeRegexExamplesTests/SSNTests.swift
new file mode 100644
index 0000000..06d33c6
--- /dev/null
+++ b/Tests/NativeRegexExamplesTests/SSNTests.swift
@@ -0,0 +1,60 @@
+import Testing
+@testable import NativeRegexExamples
+import CustomDump
+
+@Suite("SSN")
+struct SSNTests: RegexTestSuite {
+ @Test(arguments: ["123-45-6789"])
+ func wholeMatch(_ input: String) throws {
+ let wholeMatchOptional = input.wholeMatch(of: RegexLiterals.ssn)
+ let wholeMatch = try #require(wholeMatchOptional) // unwrap
+ let output = String(wholeMatch.output) // convert Substring to String
+ expectNoDifference(output, input)
+ }
+
+ @Test(
+ "NOT wholeMatch(of:)",
+ arguments: [
+ "some other text", "", "-11-1111",
+ "666-11-1111", "000-11-1111", "900-11-1111"
+ ]
+ )
+ func not_wholeMatch(_ input: String) throws {
+ let not_wholeMatch = input.wholeMatch(of: RegexLiterals.ssn)
+ #expect(
+ not_wholeMatch == nil,
+ "False positive match found: \(input) should not match \(not_wholeMatch)"
+ )
+ }
+
+ @Test("replace(_ regex: with:)")
+ func replace() {
+ var text = """
+111-11-1111 some other text
+some other text 222-22-2222
+"""
+ text.replace(RegexLiterals.ssn, with: "⬛︎⬛︎⬛︎")
+ let expected = """
+⬛︎⬛︎⬛︎ some other text
+some other text ⬛︎⬛︎⬛︎
+"""
+ expectNoDifference(expected, text)
+ }
+}
+
+@RegexActor
+func foo() {
+ let ssnRegex = RegexLiterals.ssn
+ let string = "111-11-1111"
+ string.contains(ssnRegex) // true
+ string.wholeMatch(of: ssnRegex)
+
+ var text = """
+one SSN -> 111-11-1111
+222-22-2222 <- another SSN
+"""
+ text.replace(ssnRegex, with: "⬛︎⬛︎⬛︎")
+// text is now:
+// one SSN -> ⬛︎⬛︎⬛︎
+// ⬛︎⬛︎⬛︎ <- another SSN
+}