Bronze Challenge Solutions: Disallow Alphabetic Characters

Use subSet approach

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  print("Current text: \(textField.text)")
  print("Replacement text: <\(string)> ", terminator: "")
  
  let allowedCharacterSet = CharacterSet(charactersIn: "0123456789.")
  let replacementStringCharacterSet = CharacterSet(charactersIn: string)
  if !replacementStringCharacterSet.isSubset(of: allowedCharacterSet) {
    print("Rejected (Invalid Character)")
    return false
  }

  let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
  let replacementTextHasDecimalSeparator = string.range(of: ".")
  if existingTextHasDecimalSeparator != nil,
    replacementTextHasDecimalSeparator != nil {
    print("Rejected (Already has decimal point)")
    return false
  } else {
    print("Accepted")
    return true
  }
}

Another approach.
Let swift Double() to determine whether the input is valid or not.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  print("Current text: \(textField.text)")
  print("Replacement text: <\(string)> at position \(range.location)")
  
  let newText: String
  if let oldText = textField.text {
    let startIndex = oldText.index(oldText.startIndex, offsetBy: range.location)
    let endIndex = oldText.index(startIndex, offsetBy: range.length)
    let replacementRange = startIndex..<endIndex
    newText = oldText.replacingCharacters(in: replacementRange, with: string)
  } else {
    newText = string
  }
  
  print("New text: \(newText) ", terminator: "")
  if Double(newText) != nil || newText.isEmpty || newText == "-" || newText == "." {
    print("Accepted")
    return true
  } else {
    print("Rejected")
    return false
  }
}

Another approach.
Accept inputs like: 98.6, -27, .5, -.001
Reject inputs that are not in valid number format.

Changes:

No need for class ConversionViewController to conform to the protocol UITextFieldDelegate. Remove keyword UITextFieldDelegate in class declaration.

Add a variable textBackup to store the last acceptable input.

class ConversionViewController: UIViewController {
  var textBackup = String()

Delete the function textField(: shouldChangeCharactersIn: replacementString:)
The job of checking valid input is now done by fahrenheitFieldEditingChanged(
: )

@IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
  if var text = textField.text {
    text = text.trimmingCharacters(in: .whitespaces)
    if let number = numberFormatter.number(from: text) {
      fahrenheitValue = Measurement(value: number.doubleValue, unit: .fahrenheit)
    } else {
      // Cannot convert text to valid number format
      if text.isEmpty || text == "." || text == "-" || text == "-." {
        // Accept text. Adding further input may make it a valid number
        fahrenheitValue = nil
      } else {
        // Reject text. Restore backup
        text = textBackup
      }
    }
    
    textField.text = text
    textBackup = text
  } else {
    // textField.text == nil
    fahrenheitValue = nil
  }
}

Here is my solution:

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {
    
    let letterCharacters = NSCharacterSet.letters
    let spaceCharacters = NSCharacterSet.whitespacesAndNewlines
    let punctuationAndSpecialCharacters = CharacterSet.init(charactersIn: "!#$&@~()[];,<>/?*|'\'" )
    
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
    let containLetterCharacter = string.rangeOfCharacter(from: letterCharacters)
    let containSpacesAndNewLineCharacters = string.rangeOfCharacter(from: spaceCharacters)
    let containPunctuationAndSpecialCharacters = string.rangeOfCharacter(from: punctuationAndSpecialCharacters)
    
    if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
        return false
    } else if containLetterCharacter != nil {
        return false
    } else if containSpacesAndNewLineCharacters != nil {
        return false
    } else if containPunctuationAndSpecialCharacters != nil {
        return false
    } else {
        return true
    }
}

the punctuationAndSpecialCharacters should contain the quote sign (").

let punctuationAndSpecialCharacters = CharacterSet.init(charactersIn: "\"!#$&@~()[];,<>/?*|'\'" )
1 Like

I came up with the same solution. :slight_smile:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
    
    let numbers = CharacterSet.init(charactersIn: "0123456789.")
    let thisLabel = CharacterSet.init(charactersIn: string)
    if !numbers.isSuperset(of: thisLabel) {
        return false
    }
        if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
        return false
        } else {
            return true
        }
    }

}

3 Likes

Is there a particular reason for calling the init method explicitly rather than letting the languages’s runtime system do it?

let numbers   = CharacterSet (charactersIn: "0123456789.")
let thisLabel = CharacterSet (charactersIn: string)
1 Like

Just a shorthand.

“If you specify a type by name, you can access the type’s initializer without using an initializer expression.”

The Swift Programming Language (Swift 4) / Language Reference / Expressions / Postfix Expressions / Initializer Expression

Ah. lamer

Another approach - use regex!

Add a function to ConversionViewController to detect whether you have something other than a number or decimal point,

func doesStringContainSomethingEleseOtherThanNumbersOrAPeriod(_ string: String) -> Bool {
  let regexPattern: String = "^[1-9]\\d*(\\.\\d+)?$"

  if let _ = string.range(of:regexPattern, options: .regularExpression) {
    return false
  } else {
    return true
  }
}

Then in the ConversionViewController, make this function call check before the existing check,

func textField(_ textField: UITextField, shouldChangeCharactersIn range:NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.range(of: “.”)
let replacementTextHasDecimalSeparator = string.range(of: “.”)

    if doesStringContainSomethingEleseOtherThanNumbersOrAPeriod(string) { return false }
    
    if existingTextHasDecimalSeparator != nil,  replacementTextHasDecimalSeparator != nil {
        return false
    } else {
        return true
    }
}

Looking through all these solutions it’s clear that the best way to solve this challenge is to use Int failable initializer from a string.

There’re too much hardcoding like:

let allowedCharacterSet = CharacterSet(charactersIn: “0123456789.”)

better:

let allowedCharacterSet = CharacterSet.decimalDigits

As for “.” you already have two full statement without a need to insert it into the set.

decimalDigits does not take into consideration “.”

therefore hardcoding is necessary to allow for “.” input

Another version. Using CharacterSet to define valid characters, including a minus sign to type negative numbers. Then checks so that there is only one decimal separator, only legal characters and if there is a minus sign it has to be at position 0.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    let existingStringHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalOperator = string.range(of: ".")
    let validCharacters = CharacterSet(charactersIn: ".-0123456789")
    let replacementCharacter = CharacterSet(charactersIn: string)
    
    if (existingStringHasDecimalSeparator != nil && replacementTextHasDecimalOperator != nil) || !validCharacters.isSuperset(of: replacementCharacter) || (string == "-" && range.location != 0) {
        return false
    } else {
        return true
    }
}
1 Like