diff --git a/Documentation/Evolution/StringProcessingAlgorithms.md b/Documentation/Evolution/StringProcessingAlgorithms.md index 0539d46ca..8b48b1e7d 100644 --- a/Documentation/Evolution/StringProcessingAlgorithms.md +++ b/Documentation/Evolution/StringProcessingAlgorithms.md @@ -246,12 +246,13 @@ extension BidirectionalCollection where SubSequence == Substring { /// `false`. public func contains(_ regex: some RegexComponent) -> Bool - /// Returns a Boolean value indicating whether the collection contains the - /// given regex. - /// - Parameter content: A closure to produce a `RegexComponent` to search for - /// within this collection. - /// - Returns: `true` if the regex was found in the collection, otherwise - /// `false`. + /// Returns a Boolean value indicating whether this collection contains a + /// match for the regex, where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for within + /// this collection. + /// - Returns: `true` if the regex returned by `content` matched anywhere in + /// this collection, otherwise `false`. public func contains( @RegexComponentBuilder _ content: () -> some RegexComponent ) -> Bool @@ -269,11 +270,13 @@ extension BidirectionalCollection where SubSequence == Substring { /// beginning of `regex`; otherwise, `false`. public func starts(with regex: some RegexComponent) -> Bool - /// Returns a Boolean value indicating whether the initial elements of the - /// sequence are the same as the elements in the specified regex. - /// - Parameter content: A closure to produce a `RegexComponent` to compare. - /// - Returns: `true` if the initial elements of the sequence matches the - /// beginning of `regex`; otherwise, `false`. + /// Returns a Boolean value indicating whether the initial elements of this + /// collection are a match for the regex created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to match at + /// the beginning of this collection. + /// - Returns: `true` if the initial elements of this collection match + /// regex returned by `content`; otherwise, `false`. public func starts( @RegexComponentBuilder with content: () -> some RegexComponent ) -> Bool @@ -344,11 +347,16 @@ extension BidirectionalCollection where SubSequence == Substring { /// that does not match `prefix` from the start. public func trimmingPrefix(_ regex: some RegexComponent) -> SubSequence - /// Returns a new collection of the same type by removing `prefix` from the - /// start. - /// - Parameter _content A closure to produce a `RegexComponent` to be removed. - /// - Returns: A collection containing the elements that does not match - /// `prefix` from the start. + /// Returns a subsequence of this collection by removing the elements + /// matching the regex from the start, where the regex is created by + /// the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for at + /// the start of this collection. + /// - Returns: A collection containing the elements after those that match + /// the regex returned by `content`. If the regex does not match at + /// the start of the collection, the entire contents of this collection + /// are returned. public func trimmingPrefix( @RegexComponentBuilder _ content: () -> some RegexComponent ) -> SubSequence @@ -361,8 +369,12 @@ extension RangeReplaceableCollection /// - Parameter regex: The regex to remove from this collection. public mutating func trimPrefix(_ regex: some RegexComponent) - /// Removes the initial elements that matches the given regex. - /// - Parameter content: A closure to produce a `RegexComponent` to be removed. + /// Removes the initial elements matching the regex from the start of + /// this collection, if the initial elements match, using the given closure + /// to create the regex. + /// + /// - Parameter content: A closure that returns the regex to search for + /// at the start of this collection. public mutating func trimPrefix( @RegexComponentBuilder _ content: () -> some RegexComponent ) @@ -400,12 +412,13 @@ extension BidirectionalCollection where SubSequence == Substring { /// Returns `nil` if `regex` is not found. public func firstRange(of regex: some RegexComponent) -> Range? - /// Finds and returns the range of the first occurrence of a given regex - /// within the collection. - /// - Parameter content: A closure to produce a `RegexComponent` to search for - /// within this collection. - /// - Returns: A range in the collection of the first occurrence of regex. - /// Returns `nil` if not found. + /// Returns the range of the first match for the regex within this collection, + /// where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for. + /// - Returns: A range in the collection of the first occurrence of the first + /// match of if the regex returned by `content`. Returns `nil` if no match + /// for the regex is found. public func firstRange( @RegexComponentBuilder of content: () -> some RegexComponent ) -> Range? @@ -433,12 +446,13 @@ extension BidirectionalCollection where SubSequence == Substring { /// `regex`. Returns an empty collection if `regex` is not found. public func ranges(of regex: some RegexComponent) -> some Collection> - /// Finds and returns the ranges of the all occurrences of a given sequence - /// within the collection. - /// - Parameter content: A closure to produce a `RegexComponent` to search for - /// within this collection. - /// - Returns: A collection or ranges in the receiver of all occurrences of - /// regex. Returns an empty collection if not found. + /// Returns the ranges of the all non-overlapping matches for the regex + /// within this collection, where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for. + /// - Returns: A collection of ranges of all matches for the regex returned by + /// `content`. Returns an empty collection if no match for the regex + /// is found. public func ranges( @RegexComponentBuilder of content: () -> some RegexComponent ) -> some Collection> @@ -455,10 +469,12 @@ extension BidirectionalCollection where SubSequence == Substring { /// there isn't a match. public func firstMatch(of regex: R) -> Regex.Match? - /// Returns the first match of the specified regex within the collection. - /// - Parameter content: A closure to produce a `RegexComponent` to search for. - /// - Returns: The first match of regex in the collection, or `nil` if - /// there isn't a match. + /// Returns the first match for the regex within this collection, where + /// the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for. + /// - Returns: The first match for the regex created by `content` in this + /// collection, or `nil` if no match is found. public func firstMatch( @RegexComponentBuilder of content: () -> R ) -> Regex.Match? @@ -468,8 +484,10 @@ extension BidirectionalCollection where SubSequence == Substring { /// - Returns: The match if there is one, or `nil` if none. public func wholeMatch(of regex: R) -> Regex.Match? - /// Match a regex in its entirety. - /// - Parameter content: A closure to produce a `RegexComponent` to match against. + /// Matches a regex in its entirety, where the regex is created by + /// the given closure. + /// + /// - Parameter content: A closure that returns a regex to match against. /// - Returns: The match if there is one, or `nil` if none. public func wholeMatch( @RegexComponentBuilder of content: () -> R @@ -480,8 +498,10 @@ extension BidirectionalCollection where SubSequence == Substring { /// - Returns: The match if there is one, or `nil` if none. public func prefixMatch(of regex: R) -> Regex.Match? - /// Match part of the regex, starting at the beginning. - /// - Parameter content: A closure to produce a `RegexComponent` to match against. + /// Matches part of the regex, starting at the beginning, where the regex + /// is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to match against. /// - Returns: The match if there is one, or `nil` if none. public func prefixMatch( @RegexComponentBuilder of content: () -> R @@ -498,9 +518,12 @@ extension BidirectionalCollection where SubSequence == Substring { /// - Returns: A collection of matches of `regex`. public func matches(of regex: R) -> some Collection.Match> - /// Returns a collection containing all matches of the specified regex. - /// - Parameter content: A closure to produce a `RegexComponent` to search for. - /// - Returns: A collection of matches of `regex`. + /// Returns a collection containing all non-overlapping matches of + /// the regex, created by the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for. + /// - Returns: A collection of matches for the regex returned by `content`. + /// If no matches are found, the returned collection is empty. public func matches( @RegexComponentBuilder of content: () -> R ) -> some Collection.Match> @@ -574,16 +597,20 @@ extension RangeReplaceableCollection where SubSequence == Substring { maxReplacements: Int = .max ) -> Self where Replacement.Element == Element - /// Returns a new collection in which all occurrences of a sequence matching - /// the given regex are replaced by another collection. + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closure to create the regex. + /// /// - Parameters: - /// - replacement: The new elements to add to the collection. - /// - subrange: The range in the collection in which to search for `regex`. - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. - /// - Returns: A new collection in which all occurrences of subsequence - /// matching `regex` in `subrange` are replaced by `replacement`. + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - subrange: The range in the collection in which to search for + /// the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by `replacement`, using `content` to create the regex. public func replacing( with replacement: Replacement, subrange: Range, @@ -606,15 +633,18 @@ extension RangeReplaceableCollection where SubSequence == Substring { maxReplacements: Int = .max ) -> Self where Replacement.Element == Element - /// Returns a new collection in which all occurrences of a sequence matching - /// the given regex are replaced by another collection. + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closure to create the regex. + /// /// - Parameters: - /// - replacement: The new elements to add to the collection. - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. - /// - Returns: A new collection in which all occurrences of subsequence - /// matching `regex` are replaced by `replacement`. + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of regex + /// to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by `replacement`, using `content` to create the regex. public func replacing( with replacement: Replacement, maxReplacements: Int = .max, @@ -634,18 +664,21 @@ extension RangeReplaceableCollection where SubSequence == Substring { maxReplacements: Int = .max ) where Replacement.Element == Element - /// Replaces all occurrences of the sequence matching the given regex with - /// a given collection. + /// Replaces all matches for the regex in this collection, using the given + /// closure to create the regex. + /// /// - Parameters: - /// - replacement: The new elements to add to the collection. - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. public mutating func replace( with replacement: Replacement, maxReplacements: Int = .max, @RegexComponentBuilder content: () -> some RegexComponent - ) -> Self where Replacement.Element == Element + ) where Replacement.Element == Element /// Returns a new collection in which all occurrences of a sequence matching /// the given regex are replaced by another regex match. @@ -664,18 +697,23 @@ extension RangeReplaceableCollection where SubSequence == Substring { maxReplacements: Int = .max, with replacement: (Regex.Match) throws -> Replacement ) rethrows -> Self where Replacement.Element == Element - - /// Returns a new collection in which all occurrences of a sequence matching - /// the given regex are replaced by another regex match. + + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closures to create the replacement + /// and the regex. + /// /// - Parameters: - /// - subrange: The range in the collection in which to search for `regex`. - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. + /// - subrange: The range in the collection in which to search for the + /// regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. /// - replacement: A closure that receives the full match information, - /// including captures, and returns a replacement collection. - /// - Returns: A new collection in which all occurrences of subsequence - /// matching `regex` are replaced by `replacement`. + /// including captures, and returns a replacement collection. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by the result of calling `replacement`, where regex + /// is the result of calling `content`. public func replacing( subrange: Range, maxReplacements: Int = .max, @@ -699,16 +737,20 @@ extension RangeReplaceableCollection where SubSequence == Substring { with replacement: (Regex.Match) throws -> Replacement ) rethrows -> Self where Replacement.Element == Element - /// Returns a new collection in which all occurrences of a sequence matching - /// the given regex are replaced by another collection. + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closures to create the replacement + /// and the regex. + /// /// - Parameters: - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace, using `content` to create the regex. + /// - content: A closure that returns the collection to search for + /// and replace. /// - replacement: A closure that receives the full match information, /// including captures, and returns a replacement collection. - /// - Returns: A new collection in which all occurrences of subsequence - /// matching `regex` are replaced by `replacement`. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by the result of calling `replacement`, where regex is + /// the result of calling `content`. public func replacing( maxReplacements: Int = .max, @RegexComponentBuilder content: () -> R, @@ -728,15 +770,17 @@ extension RangeReplaceableCollection where SubSequence == Substring { maxReplacements: Int = .max, with replacement: (Regex.Match) throws -> Replacement ) rethrows where Replacement.Element == Element - - /// Replaces all occurrences of the sequence matching the given regex with - /// a given collection. + + /// Replaces all matches for the regex in this collection, using the + /// given closures to create the replacement and the regex. + /// /// - Parameters: - /// - maxReplacements: A number specifying how many occurrences of the - /// sequence matching `regex` to replace. Default is `Int.max`. - /// - content: A closure to produce a `RegexComponent` to replace. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace, using `content` to create the regex. + /// - content: A closure that returns the collection to search for + /// and replace. /// - replacement: A closure that receives the full match information, - /// including captures, and returns a replacement collection. + /// including captures, and returns a replacement collection. public mutating func replace( maxReplacements: Int = .max, @RegexComponentBuilder content: () -> R, @@ -791,16 +835,16 @@ extension BidirectionalCollection where SubSequence == Substring { ) -> some Collection /// Returns the longest possible subsequences of the collection, in order, - /// around subsequence that match the given separator regex. + /// around subsequence that match the regex created by the given closure. /// /// - Parameters: - /// - maxSplits: The maximum number of times to split the collection, + /// - maxSplits: The maximum number of times to split the collection, /// or one less than the number of subsequences to return. - /// - omittingEmptySubsequences: If `false`, an empty subsequence is + /// - omittingEmptySubsequences: If `false`, an empty subsequence is /// returned in the result for each consecutive pair of matches /// and for each match at the start or end of the collection. If /// `true`, only nonempty subsequences are returned. - /// - separator: A closure to produce a `RegexComponent` to be split upon. + /// - separator: A closure that returns a regex to be split upon. /// - Returns: A collection of substrings, split from this collection's /// elements. public func split( diff --git a/Sources/RegexBuilder/Algorithms.swift b/Sources/RegexBuilder/Algorithms.swift new file mode 100644 index 000000000..f206ee768 --- /dev/null +++ b/Sources/RegexBuilder/Algorithms.swift @@ -0,0 +1,310 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021-2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _StringProcessing + +extension BidirectionalCollection where SubSequence == Substring { + /// Matches a regex in its entirety, where the regex is created by + /// the given closure. + /// + /// - Parameter content: A closure that returns a regex to match against. + /// - Returns: The match if there is one, or `nil` if none. + @available(SwiftStdlib 5.7, *) + public func wholeMatch( + @RegexComponentBuilder of content: () -> R + ) -> Regex.Match? { + wholeMatch(of: content()) + } + + /// Matches part of the regex, starting at the beginning, where the regex + /// is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to match against. + /// - Returns: The match if there is one, or `nil` if none. + @available(SwiftStdlib 5.7, *) + public func prefixMatch( + @RegexComponentBuilder of content: () -> R + ) -> Regex.Match? { + prefixMatch(of: content()) + } + + /// Returns a Boolean value indicating whether this collection contains a + /// match for the regex, where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for within + /// this collection. + /// - Returns: `true` if the regex returned by `content` matched anywhere in + /// this collection, otherwise `false`. + @available(SwiftStdlib 5.7, *) + public func contains( + @RegexComponentBuilder _ content: () -> R + ) -> Bool { + contains(content()) + } + + /// Returns the range of the first match for the regex within this collection, + /// where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for. + /// - Returns: A range in the collection of the first occurrence of the first + /// match of if the regex returned by `content`. Returns `nil` if no match + /// for the regex is found. + @available(SwiftStdlib 5.7, *) + public func firstRange( + @RegexComponentBuilder of content: () -> R + ) -> Range? { + firstRange(of: content()) + } + + // FIXME: Return `some Collection>` for SE-0346 + /// Returns the ranges of the all non-overlapping matches for the regex + /// within this collection, where the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to search for. + /// - Returns: A collection of ranges of all matches for the regex returned by + /// `content`. Returns an empty collection if no match for the regex + /// is found. + @available(SwiftStdlib 5.7, *) + public func ranges( + @RegexComponentBuilder of content: () -> R + ) -> [Range] { + ranges(of: content()) + } + + // FIXME: Return `some Collection` for SE-0346 + /// Returns the longest possible subsequences of the collection, in order, + /// around subsequence that match the regex created by the given closure. + /// + /// - Parameters: + /// - maxSplits: The maximum number of times to split the collection, + /// or one less than the number of subsequences to return. + /// - omittingEmptySubsequences: If `false`, an empty subsequence is + /// returned in the result for each consecutive pair of matches + /// and for each match at the start or end of the collection. If + /// `true`, only nonempty subsequences are returned. + /// - separator: A closure that returns a regex to be split upon. + /// - Returns: A collection of substrings, split from this collection's + /// elements. + @available(SwiftStdlib 5.7, *) + public func split( + maxSplits: Int = Int.max, + omittingEmptySubsequences: Bool = true, + @RegexComponentBuilder separator: () -> some RegexComponent + ) -> [SubSequence] { + split(separator: separator(), maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences) + } + + /// Returns a Boolean value indicating whether the initial elements of this + /// collection are a match for the regex created by the given closure. + /// + /// - Parameter content: A closure that returns a regex to match at + /// the beginning of this collection. + /// - Returns: `true` if the initial elements of this collection match + /// regex returned by `content`; otherwise, `false`. + @available(SwiftStdlib 5.7, *) + public func starts( + @RegexComponentBuilder with content: () -> R + ) -> Bool { + starts(with: content()) + } + + /// Returns a subsequence of this collection by removing the elements + /// matching the regex from the start, where the regex is created by + /// the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for at + /// the start of this collection. + /// - Returns: A collection containing the elements after those that match + /// the regex returned by `content`. If the regex does not match at + /// the start of the collection, the entire contents of this collection + /// are returned. + @available(SwiftStdlib 5.7, *) + public func trimmingPrefix( + @RegexComponentBuilder _ content: () -> R + ) -> SubSequence { + trimmingPrefix(content()) + } + + /// Returns the first match for the regex within this collection, where + /// the regex is created by the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for. + /// - Returns: The first match for the regex created by `content` in this + /// collection, or `nil` if no match is found. + @available(SwiftStdlib 5.7, *) + public func firstMatch( + @RegexComponentBuilder of content: () -> R + ) -> Regex.Match? { + firstMatch(of: content()) + } + + // FIXME: Return `some Collection.Match> for SE-0346 + /// Returns a collection containing all non-overlapping matches of + /// the regex, created by the given closure. + /// + /// - Parameter content: A closure that returns the regex to search for. + /// - Returns: A collection of matches for the regex returned by `content`. + /// If no matches are found, the returned collection is empty. + @available(SwiftStdlib 5.7, *) + public func matches( + @RegexComponentBuilder of content: () -> R + ) -> [Regex.Match] { + matches(of: content()) + } +} + +extension RangeReplaceableCollection +where Self: BidirectionalCollection, SubSequence == Substring { + /// Removes the initial elements matching the regex from the start of + /// this collection, if the initial elements match, using the given closure + /// to create the regex. + /// + /// - Parameter content: A closure that returns the regex to search for + /// at the start of this collection. + @available(SwiftStdlib 5.7, *) + public mutating func trimPrefix( + @RegexComponentBuilder _ content: () -> R + ) { + trimPrefix(content()) + } + + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closure to create the regex. + /// + /// - Parameters: + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - subrange: The range in the collection in which to search for + /// the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by `replacement`, using `content` to create the regex. + @available(SwiftStdlib 5.7, *) + public func replacing( + with replacement: Replacement, + subrange: Range, + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> some RegexComponent + ) -> Self where Replacement.Element == Element { + replacing(content(), with: replacement, subrange: subrange, maxReplacements: maxReplacements) + } + + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closure to create the regex. + /// + /// - Parameters: + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of regex + /// to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by `replacement`, using `content` to create the regex. + @available(SwiftStdlib 5.7, *) + public func replacing( + with replacement: Replacement, + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> some RegexComponent + ) -> Self where Replacement.Element == Element { + replacing(content(), with: replacement, maxReplacements: maxReplacements) + } + + /// Replaces all matches for the regex in this collection, using the given + /// closure to create the regex. + /// + /// - Parameters: + /// - replacement: The new elements to add to the collection in place of + /// each match for the regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + @available(SwiftStdlib 5.7, *) + public mutating func replace( + with replacement: Replacement, + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> some RegexComponent + ) where Replacement.Element == Element { + replace(content(), with: replacement, maxReplacements: maxReplacements) + } + + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closures to create the replacement + /// and the regex. + /// + /// - Parameters: + /// - subrange: The range in the collection in which to search for the + /// regex, using `content` to create the regex. + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - replacement: A closure that receives the full match information, + /// including captures, and returns a replacement collection. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by the result of calling `replacement`, where regex + /// is the result of calling `content`. + @available(SwiftStdlib 5.7, *) + public func replacing( + subrange: Range, + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> R, + with replacement: (Regex.Match) throws -> Replacement + ) rethrows -> Self where Replacement.Element == Element { + try replacing(content(), subrange: subrange, maxReplacements: maxReplacements, with: replacement) + } + + /// Returns a new collection in which all matches for the regex + /// are replaced, using the given closures to create the replacement + /// and the regex. + /// + /// - Parameters: + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace, using `content` to create the regex. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - replacement: A closure that receives the full match information, + /// including captures, and returns a replacement collection. + /// - Returns: A new collection in which all matches for regex in `subrange` + /// are replaced by the result of calling `replacement`, where regex is + /// the result of calling `content`. + @available(SwiftStdlib 5.7, *) + public func replacing( + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> R, + with replacement: (Regex.Match) throws -> Replacement + ) rethrows -> Self where Replacement.Element == Element { + try replacing(content(), maxReplacements: maxReplacements, with: replacement) + } + + /// Replaces all matches for the regex in this collection, using the + /// given closures to create the replacement and the regex. + /// + /// - Parameters: + /// - maxReplacements: A number specifying how many occurrences of + /// the regex to replace, using `content` to create the regex. + /// - content: A closure that returns the collection to search for + /// and replace. + /// - replacement: A closure that receives the full match information, + /// including captures, and returns a replacement collection. + @available(SwiftStdlib 5.7, *) + public mutating func replace( + maxReplacements: Int = .max, + @RegexComponentBuilder content: () -> R, + with replacement: (Regex.Match) throws -> Replacement + ) rethrows where Replacement.Element == Element { + try replace(content(), maxReplacements: maxReplacements, with: replacement) + } +} diff --git a/Sources/RegexBuilder/Match.swift b/Sources/RegexBuilder/Match.swift deleted file mode 100644 index 78a466a18..000000000 --- a/Sources/RegexBuilder/Match.swift +++ /dev/null @@ -1,45 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2021-2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -import _StringProcessing - -@available(SwiftStdlib 5.7, *) -extension String { - @available(SwiftStdlib 5.7, *) - public func wholeMatch( - @RegexComponentBuilder of content: () -> R - ) -> Regex.Match? { - wholeMatch(of: content()) - } - - @available(SwiftStdlib 5.7, *) - public func prefixMatch( - @RegexComponentBuilder of content: () -> R - ) -> Regex.Match? { - prefixMatch(of: content()) - } -} - -extension Substring { - @available(SwiftStdlib 5.7, *) - public func wholeMatch( - @RegexComponentBuilder of content: () -> R - ) -> Regex.Match? { - wholeMatch(of: content()) - } - - @available(SwiftStdlib 5.7, *) - public func prefixMatch( - @RegexComponentBuilder of content: () -> R - ) -> Regex.Match? { - prefixMatch(of: content()) - } -} diff --git a/Tests/RegexBuilderTests/AlgorithmsTests.swift b/Tests/RegexBuilderTests/AlgorithmsTests.swift index 5a7a69fac..0a2e6bc21 100644 --- a/Tests/RegexBuilderTests/AlgorithmsTests.swift +++ b/Tests/RegexBuilderTests/AlgorithmsTests.swift @@ -11,7 +11,7 @@ import XCTest import _StringProcessing -@testable import RegexBuilder +import RegexBuilder @available(SwiftStdlib 5.7, *) class RegexConsumerTests: XCTestCase { @@ -105,3 +105,339 @@ class RegexConsumerTests: XCTestCase { ) } } + +class AlgorithmsResultBuilderTests: XCTestCase { + enum MatchAlgo { + case whole + case first + case prefix + } + + enum EquatableAlgo { + case starts + case contains + case trimmingPrefix + } + + func expectMatch( + _ algo: MatchAlgo, + _ tests: (input: String, expectedCaptures: MatchType?)..., + matchType: MatchType.Type, + equivalence: (MatchType, MatchType) -> Bool, + file: StaticString = #file, + line: UInt = #line, + @RegexComponentBuilder _ content: () -> R + ) throws { + for (input, expectedCaptures) in tests { + var actual: Regex.Match? + switch algo { + case .whole: + actual = input.wholeMatch(of: content) + case .first: + actual = input.firstMatch(of: content) + case .prefix: + actual = input.prefixMatch(of: content) + } + if let expectedCaptures = expectedCaptures { + let match = try XCTUnwrap(actual, file: file, line: line) + let captures = try XCTUnwrap(match.output as? MatchType, file: file, line: line) + XCTAssertTrue(equivalence(captures, expectedCaptures), file: file, line: line) + } else { + XCTAssertNil(actual, file: file, line: line) + } + } + } + + func expectEqual( + _ algo: EquatableAlgo, + _ tests: (input: String, expected: Expected)..., + file: StaticString = #file, + line: UInt = #line, + @RegexComponentBuilder _ content: () -> R + ) throws { + for (input, expected) in tests { + var actual: Expected + switch algo { + case .contains: + actual = input.contains(content) as! Expected + case .starts: + actual = input.starts(with: content) as! Expected + case .trimmingPrefix: + actual = input.trimmingPrefix(content) as! Expected + } + XCTAssertEqual(actual, expected) + } + } + + func testMatches() throws { + let int = Capture(OneOrMore(.digit)) { Int($0)! } + + // Test syntax + let add = Regex { + int + "+" + int + } + let content = { add } + + let m = "2020+16".wholeMatch { + int + "+" + int + } + XCTAssertEqual(m?.output.0, "2020+16") + XCTAssertEqual(m?.output.1, 2020) + XCTAssertEqual(m?.output.2, 16) + + let m1 = "2020+16".wholeMatch(of: content) + XCTAssertEqual(m1?.output.0, m?.output.0) + XCTAssertEqual(m1?.output.1, m?.output.1) + XCTAssertEqual(m1?.output.2, m?.output.2) + + let firstMatch = "2020+16 0+0".firstMatch(of: content) + XCTAssertEqual(firstMatch?.output.0, "2020+16") + XCTAssertEqual(firstMatch?.output.1, 2020) + XCTAssertEqual(firstMatch?.output.2, 16) + + let prefix = "2020+16 0+0".prefixMatch(of: content) + XCTAssertEqual(prefix?.output.0, "2020+16") + XCTAssertEqual(prefix?.output.1, 2020) + XCTAssertEqual(prefix?.output.2, 16) + + try expectMatch( + .whole, + ("0+0", ("0+0", 0, 0)), + ("2020+16", ("2020+16", 2020, 16)), + ("-2020+16", nil), + ("2020+16+0+0", nil), + matchType: (Substring, Int, Int).self, + equivalence: == + ) { + int + "+" + int + } + + try expectMatch( + .prefix, + ("0+0", ("0+0", 0, 0)), + ("2020+16", ("2020+16", 2020, 16)), + ("-2020+16", nil), + ("2020+16+0+0", ("2020+16", 2020, 16)), + matchType: (Substring, Int, Int).self, + equivalence: == + ) { + int + "+" + int + } + + try expectMatch( + .first, + ("0+0", ("0+0", 0, 0)), + ("2020+16", ("2020+16", 2020, 16)), + ("-2020+16", ("2020+16", 2020, 16)), + ("2020+16+0+0", ("2020+16", 2020, 16)), + matchType: (Substring, Int, Int).self, + equivalence: == + ) { + int + "+" + int + } + } + + func testStartsAndContains() throws { + let fam = "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง we โ“‡ family" + let startsWithGrapheme = fam.starts { + OneOrMore(.anyGrapheme) + OneOrMore(.whitespace) + } + XCTAssertEqual(startsWithGrapheme, true) + + let containsDads = fam.contains { + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง" + } + XCTAssertEqual(containsDads, true) + + let content = { + Regex { + OneOrMore(.anyGrapheme) + OneOrMore(.whitespace) + } + } + XCTAssertEqual(fam.starts(with: content), true) + XCTAssertEqual(fam.contains(content), true) + + let int = Capture(OneOrMore(.digit)) { Int($0)! } + + try expectEqual( + .starts, + ("9+16, 0+3, 5+5, 99+1", true), + ("-9+16, 0+3, 5+5, 99+1", false), + (" 9+16", false), + ("a+b, c+d", false), + ("", false) + ) { + int + "+" + int + } + + try expectEqual( + .contains, + ("9+16, 0+3, 5+5, 99+1", true), + ("-9+16, 0+3, 5+5, 99+1", true), + (" 9+16", true), + ("a+b, c+d", false), + ("", false) + ) { + int + "+" + int + } + } + + func testTrim() throws { + let int = Capture(OneOrMore(.digit)) { Int($0)! } + + // Test syntax + let code = "(408)888-8888".trimmingPrefix { + "(" + OneOrMore(.digit) + ")" + } + XCTAssertEqual(code, Substring("888-8888")) + + var mutable = "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ we โ“‡ family" + mutable.trimPrefix { + .anyGrapheme + ZeroOrMore(.whitespace) + } + XCTAssertEqual(mutable, "we โ“‡ family") + + try expectEqual( + .trimmingPrefix, + ("9+16 0+3 5+5 99+1", Substring(" 0+3 5+5 99+1")), + ("a+b 0+3 5+5 99+1", Substring("a+b 0+3 5+5 99+1")), + ("0+3+5+5+99+1", Substring("+5+5+99+1")), + ("", "") + ) { + int + "+" + int + } + } + + func testReplace() { + // Test no ambiguitiy using the trailing closure + var replaced: String + let str = "9+16, 0+3, 5+5, 99+1" + replaced = str.replacing(with: "๐Ÿ”ข") { + OneOrMore(.digit) + "+" + OneOrMore(.digit) + } + XCTAssertEqual(replaced, "๐Ÿ”ข, ๐Ÿ”ข, ๐Ÿ”ข, ๐Ÿ”ข") + + replaced = str.replacing( + with: "๐Ÿ”ข", + subrange: str.startIndex..