Check string for 1 number, 1 letter and be between 5-15 characters in length

2.7k Views Asked by At

I am using the following extension to make sure a string has at least 1 number, 1 letter and between 5-15 characters in length and I feel that it can be more efficient. Any suggestions?

func checkPassword(password : String) -> Bool{

    if password.characters.count > 15 || password.characters.count < 5 {
        return false
    }


    let capitalLetterRegEx  = ".*[A-Za-z]+.*"
    let texttest = NSPredicate(format:"SELF MATCHES %@", capitalLetterRegEx)
    let capitalresult = texttest.evaluate(with: password)

    let numberRegEx  = ".*[0-9]+.*"
    let texttest1 = NSPredicate(format:"SELF MATCHES %@", numberRegEx)
    let numberresult = texttest1.evaluate(with: password)

    let specialRegEx  = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ0123456789"
    let texttest2 = NSPredicate(format:"SELF MATCHES %@", specialRegEx)
    let specialresult = !texttest2.evaluate(with: password)




    if !capitalresult || !numberresult || !specialresult  {
        return false
    }

    return true

}
2

There are 2 best solutions below

0
On

Using Regex

Regex is one approach, but if using it, we may combine your specifications into a single regex search, making use of the positive lookahead assertion technique from the following Q&A:

Here, using the regex:

^(?=.*[A-Za-z])(?=.*[0-9])(?!.*[^A-Za-z0-9]).{5,15}$

// where:
// (?=.*[A-Za-z])     Ensures string has at least one letter.
// (?=.*[0-9])        Ensures string has at least one digit.
// (?!.*[^A-Za-z0-9]) Ensures string has no invalid (non-letter/-digit) chars.
// .{5,15}            Ensures length of string is in span 5...15.

Where I've include also a negative lookahead assertion (?!...) to invalidate the password given any invalid characters.

We may implement the regex search as follows:

extension String {
    func isValidPassword() -> Bool {
        let regexInclude = try! NSRegularExpression(pattern: "^(?=.*[A-Za-z])(?=.*[0-9])(?!.*[^A-Za-z0-9]).{5,15}$")
        return regexInclude.firstMatch(in: self, options: [], range: NSRange(location: 0, length: characters.count)) != nil
    }
}

let pw1 = "hs1bés2"  // invalid character
let pw2 = "12345678" // no letters
let pw3 = "shrt"     // short
let pw4 = "A12345"   // ok

print(pw1.isValidPassword()) // false
print(pw2.isValidPassword()) // false
print(pw3.isValidPassword()) // false
print(pw4.isValidPassword()) // true

Using Set / CharacterSet

A Swift native approach is using sets of explicitly specified Character's:

extension String {
    private static var numbersSet = Set("1234567890".characters)
    private static var alphabetSet = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ".characters)

    func isValidPassword() -> Bool {
        return 5...15 ~= characters.count &&
            characters.contains(where: String.numbersSet.contains) &&
            characters.contains(where: String.alphabetSet.contains)
    }
}

Or, similarly, using the Foundation method rangeOfCharacter(from:) over CharacterSet's:

extension String {
    private static var numbersSet = CharacterSet(charactersIn: "1234567890")
    private static var alphabetSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ")

    func isValidPassword() -> Bool {
        return 5...15 ~= characters.count &&
            rangeOfCharacter(from: String.numbersSet) != nil &&
            rangeOfCharacter(from: String.alphabetSet) != nil
    }
}

If you'd also like to reject passwords that contain any character that is not in the specified sets, you could add a search operation on the (inverted) union of your sets (possibly you also allow some special characters that you'd like to include in this union). E.g., for the CharacterSet example:

extension String {
    private static var numbersSet = CharacterSet(charactersIn: "1234567890")
    private static var alphabetSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ")

    func isValidPassword() -> Bool {
        return 5...15 ~= characters.count &&
            rangeOfCharacter(from: String.numbersSet.union(String.alphabetSet).inverted) == nil &&
            rangeOfCharacter(from: String.numbersSet) != nil &&
            rangeOfCharacter(from: String.alphabetSet) != nil
    }
}

let pw1 = "hs1bés2"  // invalid character
let pw2 = "12345678" // no letter
let pw3 = "shrt"     // too short
let pw4 = "A12345"   // OK

print(pw1.isValidPassword()) // false
print(pw2.isValidPassword()) // false
print(pw3.isValidPassword()) // false
print(pw4.isValidPassword()) // true

Using pattern matching

Just for the discussion of it, yet another approach is using native Swift pattern matching:

extension String {
    private static var numberPattern = Character("0")..."9"
    private static var alphabetPattern = Character("a")..."z"

    func isValidPassword() -> Bool {
        return 5...15 ~= characters.count &&
            characters.contains { String.numberPattern ~= $0 } &&
            lowercased().characters.contains { String.alphabetPattern ~= $0 }
    }
}

let pw1 = "hs1bs2"
let pw2 = "12345678"
let pw3 = "shrt"
let pw4 = "A12345"

print(pw1.isValidPassword()) // true
print(pw2.isValidPassword()) // false
print(pw3.isValidPassword()) // false
print(pw4.isValidPassword()) // true

Just note that this approach will allow letters with diacritics (and similar) to pass as the minimum 1 letter specification, e.g.:

let diacritic: Character = "é"
print(Character("a")..."z" ~= diacritic) // true

let pw5 = "12345é6"
print(pw5.isValidPassword()) // true

as these are contained the the Character range "a"..."z"; see e.g. the excellent answer in the following thread:

0
On

Thank you for your responses. I used them to create this:

extension String {
    private static var numbersSet = Set("1234567890".characters)
    private static var alphabetSet = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ".characters)

    func isValidPassword() -> Bool {
        let characterset = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ0123456789")

        return 5...15 ~= characters.count &&
            characters.contains(where: String.numbersSet.contains) &&
            characters.contains(where: String.alphabetSet.contains) &&
            self.rangeOfCharacter(from: characterset.inverted) != nil
    }
}