Bronze Challenge Solutions: Disallow Alphabetic Characters


#1

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
  }
}

#2

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
  }
}

#3

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
  }
}

#4

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
    }
}

#5

the punctuationAndSpecialCharacters should contain the quote sign (").

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

#6

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
        }
    }

}


#7

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)

#8

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


#9

Ah. lamer


#10

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
    }
}

#11

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.


#12

decimalDigits does not take into consideration “.”

therefore hardcoding is necessary to allow for “.” input