Preg_Match Equivalent in Swift

preg_match equivalent in Swift

The array contains one element because the input contains exactly one string "PT00042H42M42S" which matches the pattern.

If you want to retrieve the matching capture groups then you have to
use rangeAtIndex: on the NSTextCheckingResult. Example:

let pattern = "P(([0-9]+)Y)?(([0-9]+)M)?(([0-9]+)D)?T?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)(.[0-9]+)?S)?"
let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)!
let text = "PT00042H42M42S"
let nsString = text as NSString
if let result = regex.firstMatchInString(text, options: nil, range: NSMakeRange(0, nsString.length)) {
for i in 0 ..< result.numberOfRanges {
let range = result.rangeAtIndex(i)
if range.location != NSNotFound {
let substring = nsString.substringWithRange(result.rangeAtIndex(i))
println("\(i): \(substring)")
}
}
}

Result:


0: PT00042H42M42S
7: 00042H
8: 00042
9: 42M
10: 42
11: 42S
12: 42

Swift extract regex matches

Even if the matchesInString() method takes a String as the first argument,
it works internally with NSString, and the range parameter must be given
using the NSString length and not as the Swift string length. Otherwise it will
fail for "extended grapheme clusters" such as "flags".

As of Swift 4 (Xcode 9), the Swift standard
library provides functions to convert between Range<String.Index>
and NSRange.

func matches(for regex: String, in text: String) -> [String] {

do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: text,
range: NSRange(text.startIndex..., in: text))
return results.map {
String(text[Range($0.range, in: text)!])
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}

Example:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]

Note: The forced unwrap Range($0.range, in: text)! is safe because
the NSRange refers to a substring of the given string text.
However, if you want to avoid it then use

        return results.flatMap {
Range($0.range, in: text).map { String(text[$0]) }
}

instead.


(Older answer for Swift 3 and earlier:)

So you should convert the given Swift string to an NSString and then extract the
ranges. The result will be converted to a Swift string array automatically.

(The code for Swift 1.2 can be found in the edit history.)

Swift 2 (Xcode 7.3.1) :

func matchesForRegexInText(regex: String, text: String) -> [String] {

do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text,
options: [], range: NSMakeRange(0, nsString.length))
return results.map { nsString.substringWithRange($0.range)}
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}

Example:

let string = "€4€9"
let matches = matchesForRegexInText("[0-9]", text: string)
print(matches)
// ["4", "9"]

Swift 3 (Xcode 8)

func matches(for regex: String, in text: String) -> [String] {

do {
let regex = try NSRegularExpression(pattern: regex)
let nsString = text as NSString
let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range)}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}

Example:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]

Equivalent of preg_match, that returns pretty (multidimensional) array

Interesting question! There's no such thing built-in, you'll have to capture offsets of matches and for each match check if its offset is within some other's bounds: something that reminds me of the nested sets model:

function match_tree($re, $subject) {
preg_match($re, $subject, $ms, PREG_OFFSET_CAPTURE);

$p = [(object)[]];

foreach($ms as $m) {
$m = (object) [
'le' => $m[1],
'ri' => $m[1] + strlen($m[0]) - 1,
'str' => $m[0]];

for($i = count($p) - 1; $i >= 0; $i--) {
if(!$i || ($m->le >= $p[$i]->le && $m->le <= $p[$i]->ri)) {
$p []= $m;
$p[$i]->sub []= $m;
break;
}
}
}

return $p[0];
}

For example,

print_r(match_tree('/(n(e?)c)(o)(f(o(abc)))/', 'XnecofoabcY'));

returns

stdClass Object
(
[sub] => Array
(
[0] => stdClass Object
(
[le] => 1
[ri] => 9
[str] => necofoabc
[sub] => Array
(
[0] => stdClass Object
(
[le] => 1
[ri] => 3
[str] => nec
[sub] => Array
(
[0] => stdClass Object
(
[le] => 2
[ri] => 2
[str] => e
)

)

)

[1] => stdClass Object
(
[le] => 4
[ri] => 4
[str] => o
)

[2] => stdClass Object
(
[le] => 5
[ri] => 9
[str] => foabc
[sub] => Array
(
[0] => stdClass Object
(
[le] => 6
[ri] => 9
[str] => oabc
[sub] => Array
(
[0] => stdClass Object
(
[le] => 7
[ri] => 9
[str] => abc
)

)

)

)

)

)

)

)

)

Check if string contains special characters in Swift

Your code check if no character in the string is from the given set.
What you want is to check if any character is not in the given set:

if (searchTerm!.rangeOfCharacterFromSet(characterSet.invertedSet).location != NSNotFound){
println("Could not handle special characters")
}

You can also achieve this using regular expressions:

let regex = NSRegularExpression(pattern: ".*[^A-Za-z0-9].*", options: nil, error: nil)!
if regex.firstMatchInString(searchTerm!, options: nil, range: NSMakeRange(0, searchTerm!.length)) != nil {
println("could not handle special characters")

}

The pattern [^A-Za-z0-9] matches a character which is not from the ranges A-Z,
a-z, or 0-9.

Update for Swift 2:

let searchTerm = "a+b"

let characterset = NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
if searchTerm.rangeOfCharacterFromSet(characterset.invertedSet) != nil {
print("string contains special characters")
}

Update for Swift 3:

let characterset = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
if searchTerm.rangeOfCharacter(from: characterset.inverted) != nil {
print("string contains special characters")
}

RegularExpression matchesInString issue in Swift

Try assigning to your results variable like this:

var results = regex.matchesInString(text, options: nil, range: NSMakeRange(0, countElements(text))) as Array<NSTextCheckingResult>

  1. the return type is Array<AnyObject>[]!, you can cast here (as in the above example) or later when you check the members of the collection

  2. in Swift options take nil to represent an empty option set (vs. 0 in Objective-C)

Regex not matching string correctly

The (?=r\/.*?(\w+)) pattern is a positive lookahead that tests each location inside a string and returns a match whenever that position is followed with r/, any 0+ chars other than line break chars as few as possible and then 1+ word chars that are captured into Group 1. You may actually grab the value if you access Group 1, but you may still keep using your code if you just change the pattern to

"(?<=\\br/)\\w+"

The pattern will check for the r/ (as a whole word) and will only return 1+ word chars that stand to the right of this char sequence.

See the online regex demo.

Pattern details

  • (?<=\br/) - a positive lookbehind that requires the presence of a

    • \b - word boundary, followed with
    • r/ - an r/ char sequence
      ... immediately to the left of the current location
  • \w+ - 1 or more word chars (letters, digits or _ char).

Swift Regex for non consecutive numbers and non repeating digits

Instead of a giant Regex, I would suggest:

  • Define a protocol:
protocol ValidationRule {

func isValid(for number: Int) -> Bool
}
  • Define a String extension to validate a given set of rules:
extension String {

func isValid(rules: [ValidationRule]) -> Bool {

guard let number = Int(self) else { return false }

for rule in rules {

if !rule.isValid(for: number) { return false }
}

return true
}
}

And then you can implement as many rules as needed. Example:

class FourDigitNumberRule: ValidationRule {
let allowedRange = 1000...9999
func isValid(for number: Int) -> Bool {
return allowedRange.contains(number)
}
}

class NoConsecutiveDigitsRule: ValidationRule {
func isValid(for number: Int) -> Bool {
let coef = 10
var remainder = number
var curr: Int? = nil
var prev: Int? = nil
var diff: Int?

while remainder > 0 {

defer { remainder = Int(remainder / coef) }
prev = curr
curr = remainder % coef
guard let p = prev, let c = curr else { continue }
let lastDiff = diff
diff = p - c
guard let ld = lastDiff else { continue }
if ld != diff { return true }
if diff != 1 && diff != -1 { return true }
}
return false
}
}

class NotYearRule: ValidationRule {

func isValid(for number: Int) -> Bool {
let hundreds = number / 100
if hundreds == 19 || hundreds == 20 {
return false
}
return true
}
}

class NonRepeatRule: ValidationRule {

func isValid(for number: Int) -> Bool {
let coef = 10
var map = [Int: Int]()
for i in 0...9 {

map[i] = 0
}
var remainder = number
while remainder > 0 {
let i = remainder % coef
map[i]! += 1
remainder = Int(remainder / coef)
}
for i in 0...9 {
if map[i]! > 2 { return false }
}
return true
}
}

And then use it like this:

let rules: [ValidationRule] = [ FourDigitNumberRule(), NoConsecutiveDigitsRule(), NotYearRule(), NonRepeatRule() ]
string.isValid(rules: rules)

Test:

let rules: [ValidationRule] = [ FourDigitNumberRule(), NoConsecutiveDigitsRule(), NotYearRule(), NonRepeatRule() ]
var test = [ "1234", "1912", "1112", "7845", "274", "14374" ]
for string in test {
let isValid = string.isValid(rules: rules) ? "valid" : "invalid"
print("\(string) is \(isValid)")
}

Output:

1234 is invalid
1912 is invalid
1112 is invalid
7845 is valid
274 is invalid
14374 is invalid

Advantages: this provides a better maintainability, and is easier to read than a giant regex you already have trouble writing and understanding...
Plus you can use the same approach for any other validation you may ever need.

Regex not working for empty string - Swift

If input is the empty string then leftover will be the empty string
as well, and therefore your function returns true. Another case where
your approach fails is

print(test("12345671234567")) // true (expected: false)

An alternative is to use the range(of:) method of String with the .regularExpression option. Then check if the matched range is the entire string.

In order to match 7 digits (and not 7 arbitrary characters), the
pattern should be \d{7}.

func test(_ input: String) -> Bool {

let pattern = "\\d{7}"
return input.range(of: pattern, options: [.regularExpression, .caseInsensitive])
== input.startIndex..<input.endIndex
}


Related Topics



Leave a reply



Submit