119

I have the need to parse some unknown data which should just be a numeric value, but may contain whitespace or other non-alphanumeric characters.

Is there a new way of doing this in Swift? All I can find online seems to be the old C way of doing things.

I am looking at stringByTrimmingCharactersInSet - as I am sure my inputs will only have whitespace/special characters at the start or end of the string. Are there any built in character sets I can use for this? Or do I need to create my own?

I was hoping there would be something like stringFromCharactersInSet() which would allow me to specify only valid characters to keep

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Dan
  • 1,343
  • 2
  • 9
  • 12

17 Answers17

245

I was hoping there would be something like stringFromCharactersInSet() which would allow me to specify only valid characters to keep.

You can either use trimmingCharacters with the inverted character set to remove characters from the start or the end of the string. In Swift 3 and later:

let result = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)

Or, if you want to remove non-numeric characters anywhere in the string (not just the start or end), you can filter the characters, e.g. in Swift 4.2.1:

let result = string.filter("0123456789.".contains)

Or, if you want to remove characters from a CharacterSet from anywhere in the string, use:

let result = String(string.unicodeScalars.filter(CharacterSet.whitespaces.inverted.contains))

Or, if you want to only match valid strings of a certain format (e.g., ####.##), you could use regular expression. For example, using the newer regular expression literals (as discussed in WWDC 2022’s video Meet Swift Regex and Swift Regex: Beyond the basics), enclosing the regex with / characters:

if let range = string.firstRange(of: /\d+(\.\d*)?/) {
    let result = string[range] // or `String(string[range])` if you need `String`
}

Or, using the old range(of:options:) with the .regularExpression option:

if let range = string.range(of: #"\d+(\.\d*)?"#, options: .regularExpression) {
    let result = string[range] // or `String(string[range])` if you need `String`
}

The behavior of these different approaches differ slightly, so it just depends on precisely what you're trying to do. Include or exclude the decimal point if you want decimal numbers, or just integers. There are lots of ways to accomplish this.


For older, Swift 2 syntax, see previous revision of this answer.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Can you explain why you need to do `inverted` on the character set in the Swift 3 example? – Andy Ibanez Jan 29 '18 at 22:40
  • 4
    @AndyIbanez It's like saying, if "ABC" are the characters I want to keep then trim everything that is not "ABC". – Scott McKenzie Apr 10 '18 at 04:30
  • 4
    In Swift 4.2.1 `let result = String(string.characters.filter { "01234567890.".characters.contains($0) })` can be shortened to `let result = string.filter("01234567890.".contains)` – Leo Dabus Feb 13 '19 at 15:10
47
let result = string.stringByReplacingOccurrencesOfString("[^0-9]", withString: "", options: NSStringCompareOptions.RegularExpressionSearch, range:nil).stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())

Swift 3

let result = string.replacingOccurrences( of:"[^0-9]", with: "", options: .regularExpression)

You can upvote this answer.

Tom Howard
  • 4,672
  • 2
  • 43
  • 48
  • 3
    Thanks! after getting all the Swift3 changes, I was left with this: myStr.replacingOccurrences( of:"[^0-9]", with: "", options: .regularExpression) – bobwki Mar 31 '17 at 23:50
34

I prefer this solution, because I like extensions, and it seems a bit cleaner to me. Solution reproduced here:

extension String {
    var digits: String {
        return components(separatedBy: CharacterSet.decimalDigits.inverted)
            .joined()
    }
}
Carien van Zyl
  • 2,853
  • 22
  • 30
Jake Cronin
  • 1,002
  • 13
  • 15
25

You can filter the UnicodeScalarView of the string using the pattern matching operator for ranges, pass a UnicodeScalar ClosedRange from 0 to 9 and initialise a new String with the resulting UnicodeScalarView:

extension String {
    private static var digits = UnicodeScalar("0")..."9"
    var digits: String {
        return String(unicodeScalars.filter(String.digits.contains))
    }
}

"abc12345".digits   // "12345"

edit/update:

Swift 4.2

extension RangeReplaceableCollection where Self: StringProtocol {
    var digits: Self {
        return filter(("0"..."9").contains)
    }
}

or as a mutating method

extension RangeReplaceableCollection where Self: StringProtocol {
    mutating func removeAllNonNumeric() {
        removeAll { !("0"..."9" ~= $0) }
    }
}

Swift 5.2 • Xcode 11.4 or later

In Swift5 we can use a new Character property called isWholeNumber:

extension RangeReplaceableCollection where Self: StringProtocol {
    var digits: Self { filter(\.isWholeNumber) }
}

extension RangeReplaceableCollection where Self: StringProtocol {
    mutating func removeAllNonNumeric() {
        removeAll { !$0.isWholeNumber }
    }
}

To allow a period as well we can extend Character and create a computed property:

extension Character {
    var isDecimalOrPeriod: Bool { "0"..."9" ~= self || self == "." }
}

extension RangeReplaceableCollection where Self: StringProtocol {
    var digitsAndPeriods: Self { filter(\.isDecimalOrPeriod) }
}

Playground testing:

"abc12345".digits   // "12345"

var str = "123abc0"
str.removeAllNonNumeric()
print(str) //"1230"

"Testing0123456789.".digitsAndPeriods // "0123456789."
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Great solution for Swift 5! How can I leave "." or "," within the String, to be able to convert a String to a Double? – Hans Bondoka Apr 17 '19 at 07:14
18

Swift 4

I found a decent way to get only alpha numeric characters set from a string. For instance:-

func getAlphaNumericValue() {

    var yourString  = "123456789!@#$%^&*()AnyThingYouWant"

    let unsafeChars = CharacterSet.alphanumerics.inverted  // Remove the .inverted to get the opposite result.  

    let cleanChars  = yourString.components(separatedBy: unsafeChars).joined(separator: "")


    print(cleanChars)  // 123456789AnyThingYouWant

}
Aashish
  • 2,532
  • 2
  • 23
  • 28
12

A solution using the filter function and rangeOfCharacterFromSet

let string = "sld [f]34é7*˜µ"

let alphaNumericCharacterSet = NSCharacterSet.alphanumericCharacterSet()
let filteredCharacters = string.characters.filter {
  return  String($0).rangeOfCharacterFromSet(alphaNumericCharacterSet) != nil
}
let filteredString = String(filteredCharacters) // -> sldf34é7µ

To filter for only numeric characters use

let string = "sld [f]34é7*˜µ"

let numericSet = "0123456789"
let filteredCharacters = string.characters.filter {
  return numericSet.containsString(String($0))
}
let filteredString = String(filteredCharacters) // -> 347

or

let numericSet : [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
let filteredCharacters = string.characters.filter {
  return numericSet.contains($0)
}
let filteredString = String(filteredCharacters) // -> 347
vadian
  • 274,689
  • 30
  • 353
  • 361
9

Swift 4

But without extensions or componentsSeparatedByCharactersInSet which doesn't read as well.

let allowedCharSet = NSCharacterSet.letters.union(.whitespaces)
let filteredText = String(sourceText.unicodeScalars.filter(allowedCharSet.contains))
Steve Moser
  • 7,647
  • 5
  • 55
  • 94
5
let string = "+1*(234) fds567@-8/90-"
let onlyNumbers = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()

print(onlyNumbers) // "1234567890"

or

extension String {

  func removeNonNumeric() -> String {
    return self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
  }
}

let onlyNumbers = "+1*(234) fds567@-8/90-".removeNonNumeric() 
print(onlyNumbers)// "1234567890"
4

Swift 3, filters all except numbers

let myString = "dasdf3453453fsdf23455sf.2234"
let result = String(myString.characters.filter { String($0).rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789")) != nil })
print(result)
CodeOverRide
  • 4,431
  • 43
  • 36
4

Swift 4.2

let numericString = string.filter { (char) -> Bool in
    return char.isNumber
}
mef27
  • 379
  • 4
  • 15
2

You can do something like this...

let string = "[,myString1. \"" // string : [,myString1. " 
let characterSet = NSCharacterSet(charactersInString: "[,. \"")
let finalString = (string.componentsSeparatedByCharactersInSet(characterSet) as NSArray).componentsJoinedByString("") 
print(finalString)   
//finalString will be "myString1"
vivek takrani
  • 3,858
  • 2
  • 19
  • 33
  • I don't have a full idea of what characters may be in the string - and I only want numeric values at the end. This would need me to list all the characters I don't want.. there are a lot of characters that could be – Dan Apr 13 '16 at 11:10
2

The issue with Rob's first solution is stringByTrimmingCharactersInSet only filters the ends of the string rather than throughout, as stated in Apple's documentation:

Returns a new string made by removing from both ends of the receiver characters contained in a given character set.

Instead use componentsSeparatedByCharactersInSet to first isolate all non-occurrences of the character set into arrays and subsequently join them with an empty string separator:

"$$1234%^56()78*9££".componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)).joinWithSeparator("")

Which returns 123456789

Ryan Brodie
  • 6,554
  • 8
  • 40
  • 57
  • No need to use `NSCharacterSet`. But your answer is the best. Here's a general version: `extension String { func removingCharactersNot(in charSet: CharacterSet) -> String { return self.components(separatedBy: charSet.inverted).joined(separator: "") } }` – xaphod May 16 '17 at 15:23
2

Swift 3

extension String {
    var keepNumericsOnly: String {
        return self.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined(separator: "")
    }
}
Ehab Saifan
  • 290
  • 2
  • 13
1

Swift 4.0 version

extension String {
    var numbers: String {
        return String(describing: filter { String($0).rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789")) != nil })
    }
}
rockdaswift
  • 9,613
  • 5
  • 40
  • 46
1

Swift 4

String.swift

import Foundation

extension String {

    func removeCharacters(from forbiddenChars: CharacterSet) -> String {
        let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) }
        return String(String.UnicodeScalarView(passed))
    }

    func removeCharacters(from: String) -> String {
        return removeCharacters(from: CharacterSet(charactersIn: from))
    }
}

ViewController.swift

let character = "1Vi234s56a78l9"
        let alphaNumericSet = character.removeCharacters(from: CharacterSet.decimalDigits.inverted)
        print(alphaNumericSet) // will print: 123456789

        let alphaNumericCharacterSet = character.removeCharacters(from: "0123456789")
        print("no digits",alphaNumericCharacterSet) // will print: Vishal
Vishal Vaghasiya
  • 4,618
  • 3
  • 29
  • 40
  • Initializing a new UnicodeScalarView is pointless. The result of the filter it is already a UnicodeScalarView. `return String(passed)` – Leo Dabus Nov 30 '17 at 12:08
1

Swift 4.2

let digitChars  = yourString.components(separatedBy:
        CharacterSet.decimalDigits.inverted).joined(separator: "")
Debaprio B
  • 503
  • 7
  • 9
0

Swift 3 Version

extension String
{
    func trimmingCharactersNot(in charSet: CharacterSet) -> String
    {
        var s:String = ""
        for unicodeScalar in self.unicodeScalars
        {
            if charSet.contains(unicodeScalar)
            {
                s.append(String(unicodeScalar))
            }
        }
        return s
    }
}
hhamm
  • 1,511
  • 15
  • 22