What Is the Replacement for Isdigit() for Characters in Swift

What is the replacement for isDigit() for characters in Swift?

The "problem" is that a Swift character does not directly correspond to a Unicode
code point, but represents an "extended grapheme cluster" which can consist of
multiple Unicode scalars. For example

let c : Character = " // REGIONAL INDICATOR SYMBOL LETTERS US

is actually a sequence of two Unicode scalars.

If we ignore this fact then you can retrieve the initial Unicode scalar of the
character (compare How can I get the Unicode code point(s) of a Character?) and test its membership in a character set:

let c : Character = "5"

let s = String(c).unicodeScalars
let uni = s[s.startIndex]

let digits = NSCharacterSet.decimalDigitCharacterSet()
let isADigit = digits.longCharacterIsMember(uni.value)

This returns "true" for the characters "0" ... "9", but actually for all
Unicode scalars of the "decimal digit category", for example:

let c1 : Character = "৯" // BENGALI DIGIT NINE U+09EF
let c2 : Character = " // MATHEMATICAL DOUBLE-STRUCK DIGIT ONE U+1D7D9

If you care only for the (ASCII) digits "0" ... "9", then the easiest method is probably:

if c >= "0" && c <= "9" { }

or, using ranges:

if "0"..."9" ~= c { }

Update: As of Swift 5 you can check for ASCII digits with

if c.isASCII && c.isNumber { }

using the “Character properties“ introduced with SE-0221.

This solves also the problem with digits modified by a variation selected U+FE0F, like the Keycap Emoji "1️⃣". (Thanks to Lukas Kukacka for reporting this problem.)

let c: Character = "1️⃣"
print(Array(c.unicodeScalars)) // ["1", "\u{FE0F}", "\u{20E3}"]
print(c.isASCII && c.isNumber) // false

How to find out if letter is Alphanumeric or Digit in Swift

For Swift 5 see rustylepord's answer.

Update for Swift 3:

let letters = CharacterSet.letters
let digits = CharacterSet.decimalDigits

var letterCount = 0
var digitCount = 0

for uni in phrase.unicodeScalars {
if letters.contains(uni) {
letterCount += 1
} else if digits.contains(uni) {
digitCount += 1
}
}

(Previous answer for older Swift versions)

A possible Swift solution:

var letterCounter = 0
var digitCount = 0
let phrase = "The final score was 32-31!"
for tempChar in phrase.unicodeScalars {
if tempChar.isAlpha() {
letterCounter++
} else if tempChar.isDigit() {
digitCount++
}
}

Update: The above solution works only with characters in the ASCII character set,
i.e. it does not recognize Ä, é or ø as letters. The following alternative
solution uses NSCharacterSet from the Foundation framework, which can test characters
based on their Unicode character classes:

let letters = NSCharacterSet.letterCharacterSet()
let digits = NSCharacterSet.decimalDigitCharacterSet()

var letterCount = 0
var digitCount = 0

for uni in phrase.unicodeScalars {
if letters.longCharacterIsMember(uni.value) {
letterCount++
} else if digits.longCharacterIsMember(uni.value) {
digitCount++
}
}

Update 2: As of Xcode 6 beta 4, the first solution does not work anymore, because
the isAlpha() and related (ASCII-only) methods have been removed from Swift.
The second solution still works.

In Swift, how do I sort an array of strings and have strings of numbers, symbols, etc. always come after alphabetic strings?

The key is to write your "is ordered before" function to do whatever you want. For example, if by digits, you mean "0"..."9", then something like this is probably what you want:

func isDigit(c: Character) -> Bool {
return "0" <= c && c <= "9"
}

func sortedLettersFirst(lhs: String, rhs: String) -> Bool {
for (lc, rc) in zip(lhs.characters, rhs.characters) {
if lc == rc { continue }

if isDigit(lc) && !isDigit(rc) {
return false
}
if !isDigit(lc) && isDigit(rc) {
return true
}
return lc < rc
}
return lhs.characters.count < rhs.characters.count
}

words.sort(sortedLettersFirst)

Of course, if by "digit" you mean "unicode digits", then see What is the replacement for isDigit() for characters in Swift? for a different approach to isDigit. But ultimately, the point is to make whatever rule you want in your isOrderedBefore function, and pass that to sort().

How to achieve that placeholder text disappears character by character in UITextField

I don't believe the default behavior of the placeholder is editable, but what you are trying to accomplish can be done using NSAttributedString to simulate the placeholder value.

I'm sure this can be optimized, but here I have created a handler class that acts as the delegate for a given UITextField, manipulating the string the user inputs to achieve the desired effect. You init the handler with your desired placeholder string, so you can make any text field work this way.

Custom Placeholder Text Field

import UIKit

class CustomPlaceholderTextFieldHandler: NSObject {
let placeholderText: String
let placeholderAttributes = [NSForegroundColorAttributeName : UIColor.lightGray]
let inputAttributes = [NSForegroundColorAttributeName : UIColor(red: 255/255, green: 153/255, blue: 0, alpha: 1.0)]
var input = ""

init(placeholder: String) {
self.placeholderText = placeholder
super.init()
}

func resetPlaceholder(for textField: UITextField) {
input = ""
setCombinedText(for: textField)
}

fileprivate func setCursorPosition(for textField: UITextField) {
guard let cursorPosition = textField.position(from: textField.beginningOfDocument, offset: input.characters.count)
else { return }

textField.selectedTextRange = textField.textRange(from: cursorPosition, to: cursorPosition)
}

fileprivate func setCombinedText(for textField: UITextField) {
let placeholderSubstring = placeholderText.substring(from: input.endIndex)
let attributedString = NSMutableAttributedString(string: input + placeholderSubstring, attributes: placeholderAttributes)
attributedString.addAttributes(inputAttributes, range: NSMakeRange(0, input.characters.count))
textField.attributedText = attributedString
}
}

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

if string == "" {
if input.characters.count > 0 {
input = input.substring(to: input.index(before: input.endIndex))
}
} else {
input += string
}

if input.characters.count <= placeholderText.characters.count {
setCombinedText(for: textField)
setCursorPosition(for: textField)
return false
}
return true
}

func textFieldDidBeginEditing(_ textField: UITextField) {
setCursorPosition(for: textField)
}
}

Here's a the way I initialized the gif above:

class ViewController: UIViewController {

@IBOutlet weak var textField: UITextField!
let placeholderHandler = CustomPlaceholderTextFieldHandler(placeholder: "_2_-__-__A")

override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = placeholderHandler
placeholderHandler.resetPlaceholder(for: textField)
}
}

This could be expanded to take color parameters, fonts, etc. at initialization, or you may find it cleaner to subclass UITextField and make it its own delegate. I also haven't really tested this for selecting/deleting/replacing multiple characters.

The input variable will return the text the user has input at any given point. Also, using a fixed-width font would remove the jitteriness as the user types and replaces the placeholder text.

Formatting a UITextField for credit card input like (xxxx xxxx xxxx xxxx)

If you're using Swift, go read my port of this answer for Swift 4 and use that instead.

If you're in Objective-C...

Firstly, to your UITextFieldDelegate, add these instance variables...

NSString *previousTextFieldContent;
UITextRange *previousSelection;

... and these methods:

// Version 1.3
// Source and explanation: http://stackoverflow.com/a/19161529/1709587
-(void)reformatAsCardNumber:(UITextField *)textField
{
// In order to make the cursor end up positioned correctly, we need to
// explicitly reposition it after we inject spaces into the text.
// targetCursorPosition keeps track of where the cursor needs to end up as
// we modify the string, and at the end we set the cursor position to it.
NSUInteger targetCursorPosition =
[textField offsetFromPosition:textField.beginningOfDocument
toPosition:textField.selectedTextRange.start];

NSString *cardNumberWithoutSpaces =
[self removeNonDigits:textField.text
andPreserveCursorPosition:&targetCursorPosition];

if ([cardNumberWithoutSpaces length] > 19) {
// If the user is trying to enter more than 19 digits, we prevent
// their change, leaving the text field in its previous state.
// While 16 digits is usual, credit card numbers have a hard
// maximum of 19 digits defined by ISO standard 7812-1 in section
// 3.8 and elsewhere. Applying this hard maximum here rather than
// a maximum of 16 ensures that users with unusual card numbers
// will still be able to enter their card number even if the
// resultant formatting is odd.
[textField setText:previousTextFieldContent];
textField.selectedTextRange = previousSelection;
return;
}

NSString *cardNumberWithSpaces =
[self insertCreditCardSpaces:cardNumberWithoutSpaces
andPreserveCursorPosition:&targetCursorPosition];

textField.text = cardNumberWithSpaces;
UITextPosition *targetPosition =
[textField positionFromPosition:[textField beginningOfDocument]
offset:targetCursorPosition];

[textField setSelectedTextRange:
[textField textRangeFromPosition:targetPosition
toPosition:targetPosition]
];
}

-(BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
{
// Note textField's current state before performing the change, in case
// reformatTextField wants to revert it
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;

return YES;
}

/*
Removes non-digits from the string, decrementing `cursorPosition` as
appropriate so that, for instance, if we pass in `@"1111 1123 1111"`
and a cursor position of `8`, the cursor position will be changed to
`7` (keeping it between the '2' and the '3' after the spaces are removed).
*/
- (NSString *)removeNonDigits:(NSString *)string
andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
NSUInteger originalCursorPosition = *cursorPosition;
NSMutableString *digitsOnlyString = [NSMutableString new];
for (NSUInteger i=0; i<[string length]; i++) {
unichar characterToAdd = [string characterAtIndex:i];
if (isdigit(characterToAdd)) {
NSString *stringToAdd =
[NSString stringWithCharacters:&characterToAdd
length:1];

[digitsOnlyString appendString:stringToAdd];
}
else {
if (i < originalCursorPosition) {
(*cursorPosition)--;
}
}
}

return digitsOnlyString;
}

/*
Detects the card number format from the prefix, then inserts spaces into
the string to format it as a credit card number, incrementing `cursorPosition`
as appropriate so that, for instance, if we pass in `@"111111231111"` and a
cursor position of `7`, the cursor position will be changed to `8` (keeping
it between the '2' and the '3' after the spaces are added).
*/
- (NSString *)insertCreditCardSpaces:(NSString *)string
andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
// Mapping of card prefix to pattern is taken from
// https://baymard.com/checkout-usability/credit-card-patterns

// UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
bool is456 = [string hasPrefix: @"1"];

// These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all
// these as 4-6-5-4 to err on the side of always letting the user type more
// digits.
bool is465 = [string hasPrefix: @"34"] ||
[string hasPrefix: @"37"] ||

// Diners Club
[string hasPrefix: @"300"] ||
[string hasPrefix: @"301"] ||
[string hasPrefix: @"302"] ||
[string hasPrefix: @"303"] ||
[string hasPrefix: @"304"] ||
[string hasPrefix: @"305"] ||
[string hasPrefix: @"309"] ||
[string hasPrefix: @"36"] ||
[string hasPrefix: @"38"] ||
[string hasPrefix: @"39"];

// In all other cases, assume 4-4-4-4-3.
// This won't always be correct; for instance, Maestro has 4-4-5 cards
// according to https://baymard.com/checkout-usability/credit-card-patterns,
// but I don't know what prefixes identify particular formats.
bool is4444 = !(is456 || is465);

NSMutableString *stringWithAddedSpaces = [NSMutableString new];
NSUInteger cursorPositionInSpacelessString = *cursorPosition;
for (NSUInteger i=0; i<[string length]; i++) {
bool needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15));
bool needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15));
bool needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0);

if (needs465Spacing || needs456Spacing || needs4444Spacing) {
[stringWithAddedSpaces appendString:@" "];
if (i < cursorPositionInSpacelessString) {
(*cursorPosition)++;
}
}
unichar characterToAdd = [string characterAtIndex:i];
NSString *stringToAdd =
[NSString stringWithCharacters:&characterToAdd length:1];

[stringWithAddedSpaces appendString:stringToAdd];
}

return stringWithAddedSpaces;
}

Secondly, set reformatCardNumber: to be called whenever the text field fires a UIControlEventEditingChanged event:

[yourTextField addTarget:yourTextFieldDelegate 
action:@selector(reformatAsCardNumber:)
forControlEvents:UIControlEventEditingChanged];

(Of course, you'll need to do this at some point after your text field and its delegate have been instantiated. If you're using storyboards, the viewDidLoad method of your view controller is an appropriate place.

Some Explanation

This is a deceptively complicated problem. Three important issues that may not be immediately obvious (and which previous answers here all fail to take into account):

  1. While the XXXX XXXX XXXX XXXX format for credit and debit card numbers is the most common one, it's not the only one. For example, American Express cards have 15 digit numbers usually written in XXXX XXXXXX XXXXX format, like this:

    An American Express card

    Even Visa cards can have fewer than 16 digits, and Maestro cards can have more:

    A Russian Maestro card with 18 digits

  2. There are more ways for the user to interact with a text field than just typing in single characters at the end of their existing input. You also have to properly handle the user adding characters in the middle of the string, deleting single characters, deleting multiple selected characters, and pasting in multiple characters. Some simpler/more naive approaches to this problem will fail to handle some of these interactions properly. The most perverse case is a user pasting in multiple characters in the middle of the string to replace other characters, and this solution is general enough to handle that.

  3. You don't just need to reformat the text of the text field properly after the user modifies it - you also need to position the text cursor sensibly. Naive approaches to the problem that don't take this into account will almost certainly end up doing something silly with the text cursor in some cases (like putting it to the end of the text field after the user adds a digit in the middle of it).

To deal with issue #1, we use the partial mapping of card number prefixes to formats curated by The Baymard Institute at https://baymard.com/checkout-usability/credit-card-patterns. We can automatically detect the the card provider from the first couple of digits and (in some cases) infer the format and adjust our formatting accordingly. Thanks to cnotethegr8 for contributing this idea to this answer.

The simplest and easiest way to deal with issue #2 (and the way used in the code above) is to strip out all spaces and reinsert them in the correct positions every time the content of the text field changes, sparing us the need to figure out what kind of text manipulation (an insertion, a deletion, or a replacement) is going on and handle the possibilities differently.

To deal with issue #3, we keep track of how the desired index of the cursor changes as we strip out non-digits and then insert spaces. This is why the code rather verbosely performs these manipulations character-by-character using NSMutableString, rather than using NSString's string replacement methods.

Finally, there's one more trap lurking: returning NO from textField: shouldChangeCharactersInRange: replacementString breaks the 'Cut' button the user gets when they select text in the text field, which is why I don't do it. Returning NO from that method results in 'Cut' simply not updating the clipboard at all, and I know of no fix or workaround. As a result, we need to do the reformatting of the text field in a UIControlEventEditingChanged handler instead of (more obviously) in shouldChangeCharactersInRange: itself.

Luckily, the UIControl event handlers seem to get called before UI updates get flushed to the screen, so this approach works fine.

There are also a whole bunch of minor questions about exactly how the text field should behave that don't have obvious correct answers:

  • If the user tries to paste in something that would cause the content of the text field to exceed 19 digits, should the beginning of the pasted string be inserted (until 19 digits are reached) and the remainder cropped, or should nothing be inserted at all?
  • If the user tries to delete a single space by positioning their cursor after it and pressing the backspace key, should nothing happen and the cursor remain where it is, should the cursor move left one character (placing it before the space), or should the digit to the left of the space be deleted as though the cursor were already left of the space?
  • When the user types in the fourth, eighth, or twelfth digit, should a space be immediately inserted and the cursor moved after it, or should the space only be inserted after the user types the fifth, ninth, or thirteenth digit?
  • When the user deletes the first digit after a space, if this doesn't cause the space to be removed entirely, should this lead to their cursor being positioned before or after the space?

Probably any answer to any of these questions will be adequate, but I list them just to make clear that there are actually a lot of special cases that you might want to think carefully about here, if you were obsessive enough. In the code above, I've picked answers to these questions that seemed reasonable to me. If you happen to have strong feelings about any of these points that aren't compatible with the way my code behaves, it should be easy enough to tweak it to your needs.

Objective-C: Find numbers in string

Here's an NSScanner based solution:

// Input
NSString *originalString = @"This is my string. #1234";

// Intermediate
NSString *numberString;

NSScanner *scanner = [NSScanner scannerWithString:originalString];
NSCharacterSet *numbers = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];

// Throw away characters before the first number.
[scanner scanUpToCharactersFromSet:numbers intoString:NULL];

// Collect numbers.
[scanner scanCharactersFromSet:numbers intoString:&numberString];

// Result.
int number = [numberString integerValue];

(Some of the many) assumptions made here:

  • Number digits are 0-9, no sign, no decimal point, no thousand separators, etc. You could add sign characters to the NSCharacterSet if needed.
  • There are no digits elsewhere in the string, or if there are they are after the number you want to extract.
  • The number won't overflow int.

Alternatively you could scan direct to the int:

[scanner scanUpToCharactersFromSet:numbers intoString:NULL];
int number;
[scanner scanInt:&number];

If the # marks the start of the number in the string, you could find it by means of:

[scanner scanUpToString:@"#" intoString:NULL];
[scanner setScanLocation:[scanner scanLocation] + 1];
// Now scan for int as before.

mask all digits except first 6 and last 4 digits of a string( length varies )

This will work with any card number length:

var cardNumber = "3456123434561234";

var firstDigits = cardNumber.Substring(0, 6);
var lastDigits = cardNumber.Substring(cardNumber.Length - 4, 4);

var requiredMask = new String('X', cardNumber.Length - firstDigits.Length - lastDigits.Length);

var maskedString = string.Concat(firstDigits, requiredMask, lastDigits);
var maskedCardNumberWithSpaces = Regex.Replace(maskedString, ".{4}", "$0 ");


Related Topics



Leave a reply



Submit