Get numbers characters from a string

Pattern matching a String's unicode scalars against Western Arabic Numerals

You could pattern match the unicodeScalars view of a String to a given UnicodeScalar pattern (covering e.g. Western Arabic numerals).

extension String {
    var westernArabicNumeralsOnly: String {
        let pattern = UnicodeScalar("0")..."9"
        return String(unicodeScalars
            .flatMap { pattern ~= $0 ? Character($0) : nil })
    }
}

Example usage:

let str1 = "string_1"
let str2 = "string_20_certified"
let str3 = "a_1_b_2_3_c34"

let newStr1 = str1.westernArabicNumeralsOnly
let newStr2 = str2.westernArabicNumeralsOnly
let newStr3 = str3.westernArabicNumeralsOnly

print(newStr1) // 1
print(newStr2) // 20
print(newStr3) // 12334

Extending to matching any of several given patterns

The unicode scalar pattern matching approach above is particularly useful extending it to matching any of a several given patterns, e.g. patterns describing different variations of Eastern Arabic numerals:

extension String {   
    var easternArabicNumeralsOnly: String {
        let patterns = [UnicodeScalar("\u{0660}")..."\u{0669}", // Eastern Arabic
                                       "\u{06F0}"..."\u{06F9}"] // Perso-Arabic variant 
        return String(unicodeScalars
            .flatMap { uc in patterns.contains{ $0 ~= uc } ? Character(uc) : nil })
    }
}

This could be used in practice e.g. if writing an Emoji filter, as ranges of unicode scalars that cover emojis can readily be added to the patterns array in the Eastern Arabic example above.


Why use the UnicodeScalar patterns approach over Character ones?

A Character in Swift contains of an extended grapheme cluster, which is made up of one or more Unicode scalar values. This means that Character instances in Swift does not have a fixed size in the memory, which means random access to a character within a collection of sequentially (/contiguously) stored character will not be available at O(1), but rather, O(n).

Unicode scalars in Swift, on the other hand, are stored in fixed sized UTF-32 code units, which should allow O(1) random access. Now, I'm not entirely sure if this is a fact, or a reason for what follows: but a fact is that if benchmarking the methods above vs equivalent method using the CharacterView (.characters property) for some test String instances, its very apparent that the UnicodeScalar approach is faster than the Character approach; naive testing showed a factor 10-25 difference in execution times, steadily growing for growing String size.

Knowing the limitations of working with Unicode scalars vs Characters in Swift

Now, there are drawbacks using the UnicodeScalar approach, however; namely when working with characters that cannot represented by a single unicode scalar, but where one of its unicode scalars are contained in the pattern to which we want to match.

E.g., consider a string holding the four characters "Café". The last character, "é", is represented by two unicode scalars, "e" and "\u{301}". If we were to implement pattern matching against, say, UnicodeScalar("a")...e, the filtering method as applied above would allow one of the two unicode scalars to pass.

extension String {
    var onlyLowercaseLettersAthroughE: String {
        let patterns = [UnicodeScalar("1")..."e"]
        return String(unicodeScalars
            .flatMap { uc in patterns.contains{ $0 ~= uc } ? Character(uc) : nil })
    }
}

let str = "Cafe\u{301}"
print(str)                               // Café
print(str.onlyLowercaseLettersAthroughE) // Cae
                                         /* possibly we'd want "Ca" or "Caé"
                                            as result here                   */

In the particular use case queried by from the OP in this Q&A, the above is not an issue, but depending on the use case, it will sometimes be more appropriate to work with Character pattern matching over UnicodeScalar.


Edit: Updated for Swift 4 & 5

Here's a straightforward method that doesn't require Foundation:

let newstring = string.filter { "0"..."9" ~= $0 }

or borrowing from @dfri's idea to make it a String extension:

extension String {
    var numbers: String {
        return filter { "0"..."9" ~= $0 }
    }
}

print("3 little pigs".numbers) // "3"
print("1, 2, and 3".numbers)   // "123"

import Foundation

let string = "a_1_b_2_3_c34"    
let result = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "")
print(result)

Output:

12334

Tags:

Swift

Swift3