Converting String using specific encoding to get just one character

Viewed 126

I'm on this frustrating journey trying to get a specific character from a Swift string. I have an Objective-C function, something like

- ( NSString * ) doIt: ( char ) c

that I want to call from Swift.

This c is eventually passed to a C function in the back that does the weightlifting here but this function gets tripped over when c is   or A0.

Now I have two questions (apologies SO).

  1. I am trying to use different encodings, especially the ASCII variants, hoping one would convert   (A0) to spcae (20 or dec 32). The verdict seems to be that I need to hardcode this but if there is a failsafe, non-hardcoded way I'd like to hear about it!

  2. I am really struggling with the conversion itself. How do I access a specific character using a specific encoding in Swift?

a) I can use

s.utf8CString[ i ]

but then I am bound to UTF8.

b) I can use something like

let s = "\u{a0}"
let p = UnsafeMutablePointer < CChar >.allocate ( capacity : n )

defer
{
    p.deallocate()
}

// Convert to ASCII
NSString ( string : s ).getCString ( p,
        maxLength : n,
        encoding  : CFStringConvertEncodingToNSStringEncoding ( CFStringBuiltInEncodings.ASCII.rawValue ) )

// Hope for 32
let c = p[ i ]

but this seems overkill. The string is converted to NSString to apply the encoding and I need to allocate a pointer, all just to get a single character.

c) Here it seems Swift String's withCString is the man for the job, but I can not even get it to compile. Below is what Xcode's completion gives but even after fiddling with it for a long time I am still stuck.

// How do I use this
// ??
s.withCString ( encodedAs : _UnicodeEncoding.Protocol ) { ( UnsafePointer < FixedWidthInteger & UnsignedInteger > ) -> Result in
// ??
}

TIA

1 Answers

There are two withCString() methods: withCString(_:) calls the given closure with a pointer to the contents of the string, represented as a null-terminated sequence of UTF-8 code units. Example:

// An emulation of your Objective-C method.
func doit(_ c: CChar) {
    print(c, terminator: " ")
}

let s = "a\u{A0}b"
s.withCString { ptr in
    var p = ptr
    while p.pointee != 0 {
        doit(p.pointee)
        p += 1
    }
}
print()

// Output: 97 -62 -96 98

Here -62 -96 is the signed character representation of the UTF-8 sequence C2 A0 of the NO-BREAK SPACE character U+00A0.

If you just want to iterate over all UTF-8 characters of the string sequentially then you can simply use the .utf8 view. The (unsigned) UInt8 bytes must be converted to the corresponding (signed) CChar:

let s = "a\u{A0}b"
for c in s.utf8 {
        doit(CChar(bitPattern: c))
}
print()

I am not aware of a method which transforms U+00A0 to a “normal” space character, so you have to do that manually. With

let s = "a\u{A0}b".replacingOccurrences(of: "\u{A0}", with: " ")

the output of the above program would be 97 32 98.

The withCString(encodedAs:_:) method calls the given closure with a pointer to the contents of the string, represented as a null-terminated sequence of code units. Example:

let s = "a\u{A0}b€"
s.withCString(encodedAs: UTF16.self) { ptr in
    var p = ptr
    while p.pointee != 0 {
        print(p.pointee, terminator: " ")
        p += 1
    }
}
print()

// Output: 97 160 98 8364

This method is probably of limited use for your purpose because it can only be used with UTF8, UTF16 and UTF32.

For other encodings you can use the data(using:) method. It produces a Data value which is a sequence of UInt8 (an unsigned type). As above, these must be converted to the corresponding signed character:

let s = "a\u{A0}b"
if let data = s.data(using: .isoLatin1) {
    data.forEach {
        doit(CChar(bitPattern: $0))
    }
}
print()

// Output: 97 -96 98

Of course this may fail if the string is not representable in the given encoding.

Related