Cannot spell check words of less than seven characters with UITextChecker - swift

I'm attempting to check whether a word is in the dictionary with the following function
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let wordRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return wordRange.location == NSNotFound
}
The problem is that this only works correctly for words of seven characters or more. Shorter words return true even if they are not in the dictionary. Specifically, we get wordRange = {9223372036854775807, 0} in this case, the same as for a valid word.

The solution turns out to be embarrassingly simple. Our strings were upper case, and UITextChecker treats any upper case string shorter than seven characters as a possible valid acronym. In lower case everything works as expected.

I find it works just fine, when I use your function in a Swift Playground:
import UIKit
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let wordRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return wordRange.location == NSNotFound
}
let validStrings = ["test", "fest", "fast"]
let validResults = validStrings.map{ isReal(word:$0) }
print(validResults)
let invalidStrings = ["xt", "fxxx", "srwe"]
let invalidResults = invalidStrings.map{ isReal(word:$0) }
print(invalidResults)
Your issue may be platform or version specific.

Related

If the text contains emoji, Range is nil from swift

The text "Welcome my application..❣️" does not make sense during the NSRange and Range tests. If ❣️ is included, Range is returned as nil, and I wonder why.
func testA() {
let testStr = "Welcome my application..❣️"
let range = NSRange(location: 0, length: testStr.count)
let wrapRange = Range(range, in: testStr)
let testStrB = "Welcome my application.."
let rangeB = NSRange(location: 0, length: testStrB.count)
let wrapRangeB = Range(rangeB, in: testStrB)
print("wrapRange: \(wrapRange) wrapRangeB: \(wrapRangeB)")
}
RESULT:
wrapRange: nil wrapRangeB: Optional(Range(Swift.String.Index(_rawBits: 1)..<Swift.String.Index(_rawBits: 1572864)))
"❣️" is a single “extended grapheme cluster”, but two UTF-16 code units:
print("❣️".count) // 1
print("❣️".utf16.count) // 2
NSRange counts UTF-16 code units (which are the “characters” in an NSString) , therefore the correct way to create an NSRange comprising the complete range of a Swift string is
let range = NSRange(location: 0, length: testStr.utf16.count)
or better (since Swift 4):
let range = NSRange(testStr.startIndex..., in: testStr)
Explanation: In your code (simplified here)
let testStr = "❣️"
let range = NSRange(location: 0, length: testStr.count)
print(range) // {0, 1}
creates an NSRange describing a single UTF-16 code unit. This cannot be converted to a Range<String.Index> in testStr because its first Character consists of two UTF-16 code units:
let wrapRange = Range(range, in: testStr)
print(wrapRange) // nil

Converting numbers to string in a given string in Swift

I am given a string like 4eysg22yl3kk and my output should be like this:
foureysgtweny-twoylthreekk or if I am given 0123 it should be output as one hundred twenty-three. So basically, as I scan the string, I need to convert numbers to string.
I do not know how to implement this in Swift as I iterate through the string? Any idea?
You actually have two basic problems.
The first is convert a "number" to "spelt out" value (ie 1 to one). This is actually easy to solve, as NumberFormatter has a spellOut style property
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let text = formatter.string(from: NSNumber(value: 1))
which will result in "one", neat.
The other issue though, is how to you separate the numbers from the text?
While I can find any number of solutions for "extract" numbers or characters from a mixed String, I can't find one which return both, split on their boundaries, so, based on your input, we'd end up with ["4", "eysg", "22", "yl", "3", "kk"].
So, time to role our own...
func breakApart(_ text: String, withPattern pattern: String) throws -> [String]? {
do {
let regex = try NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)
var previousRange: Range<String.Index>? = nil
var parts: [String] = []
for match in regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) {
guard let range = Range(match.range, in: text) else {
return nil
}
let part = text[range]
if let previousRange = previousRange {
let textRange = Range<String.Index>(uncheckedBounds: (lower: previousRange.upperBound, upper: range.lowerBound))
parts.append(String(text[textRange]))
}
parts.append(String(part))
previousRange = range
}
if let range = previousRange, range.upperBound != text.endIndex {
let textRange = Range<String.Index>(uncheckedBounds: (lower: range.upperBound, upper: text.endIndex))
parts.append(String(text[textRange]))
}
return parts
} catch {
}
return nil
}
Okay, so this is a little "dirty" (IMHO), but I can't seem to think of a better approach, hopefully someone will be kind enough to provide some hints towards one ;)
Basically what it does is uses a regular expression to find all the groups of numbers, it then builds an array, cutting the string apart around the matching boundaries - like I said, it's crude, but it gets the job done.
From there, we just need to map the results, spelling out the numbers as we go...
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let value = "4eysg22yl3kk"
if let parts = try breakApart(value, withPattern: pattern) {
let result = parts.map { (part) -> String in
if let number = Int(part), let text = formatter.string(from: NSNumber(value: number)) {
return text
}
return part
}.joined(separator: " ")
print(result)
}
This will end up printing four eysg twenty-two yl three kk, if you don't want the spaces, just get rid of separator in the join function
I did this in Playgrounds, so it probably needs some cleaning up
I was able to solve my question without dealing with anything extra than converting my String to an array and check char by char. If I found a digit I was saving it in a temp String and as soon as I found out the next char is not digit, I converted my digit to its text.
let inputString = Array(string.lowercased())

How to find Multiple NSRange for a string from full string iOS swift

let fullString = "Hello world, there are \(string(07)) continents and \(string(195)) countries."
let range = [NSMakeRange(24,2), NSMakeRange(40,3)]
Need to find the NSRange for numbers in the entire full string and there is a possibility that both numbers can be same. Currently hard coding like shown above, the message can be dynamic where hard coding values will be problematic.
I have split the strings and try to fetch NSRange since there is a possibility of same value. like stringOne and stringTwo.
func findNSMakeRange(initialString:String, fromString: String) {
let fullStringRange = fromString.startIndex..<fromString.endIndex
fromString.enumerateSubstrings(in: fullStringRange, options: NSString.EnumerationOptions.byWords) { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(fromString.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
if (substring == initialString) {
print(substring, range)
}
})
}
Receiving errors like Cannot invoke distance with an argument list of type (String.Index, String.Index)
Anyone have any better solution ?
You say that you want to iterate through NSRange matches in a string so that you can apply a bold attribute to the relevant substrings.
In Swift 5.7 and later, you can use the new Regex:
string.ranges(of: /\d+/)
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}
Or if you find the traditional regular expressions too cryptic, you can import RegexBuilder, and you can use the new regex DSL:
string.ranges(of: Regex { OneOrMore(.digit) })
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}
In Swift versions prior to 5.7, one would use NSRegularExpression. E.g.:
let range = NSRange(location: 0, length: string.count)
try! NSRegularExpression(pattern: "\\d+").enumerateMatches(in: string, range: range) { result, _, _ in
guard let range = result?.range else { return }
attributedString.setAttributes(attributes, range: range)
}
Personally, before Swift 5.7, I found it useful to have a method to return an array of Swift ranges, i.e. [Range<String.Index>]:
extension StringProtocol {
func ranges<T: StringProtocol>(of string: T, options: String.CompareOptions = []) -> [Range<Index>] {
var ranges: [Range<Index>] = []
var start: Index = startIndex
while let range = range(of: string, options: options, range: start ..< endIndex) {
ranges.append(range)
if !range.isEmpty {
start = range.upperBound // if not empty, resume search at upper bound
} else if range.lowerBound < endIndex {
start = index(after: range.lowerBound) // if empty and not at end, resume search at next character
} else {
break // if empty and at end, then quit
}
}
return ranges
}
}
Then you can use it like so:
let string = "Hello world, there are 09 continents and 195 countries."
let ranges = string.ranges(of: "[0-9]+", options: .regularExpression)
And then you can map the Range to NSRange. Going back to the original example, if you wanted to make these numbers bold in some attributed string:
string.ranges(of: "[0-9]+", options: .regularExpression)
.map { NSRange($0, in: string) }
.forEach { attributedString.setAttributes(boldAttributes, range: $0) }
Resources:
Swift 5.7 and later:
WWDC 2022 video Meet Swift Regex
WWDC 2022 video Swift Regex: Beyond the basics
Hacking With Swift: Regular Expressions
Swift before 5.7:
Hacking With Swift: How to use regular expressions in Swift
NSHipster: Regular Expressions in Swift

Swift. Add attributes to multiple instances

I have arrays containing strings of the text terms to which I want to apply a particular attribute. Here's a code snippit:
static var bold = [String]()
static let boldAttribs = [NSFontAttributeName: UIFont(name: "WorkSans-Medium", size: 19)!]
for term in bold {
atStr.addAttributes(boldAttribs, range: string.rangeOfString(term))
}
This works great for single term or phrase use. But it only applies to the first use of a specific term. Is there a way, without resorting to numerical ranges, to apply the attribute to all instances of the same term? For example, make every use of "animation button" within the same string bold.
Edit: This works.
// `butt2` is [String]() of substrings to attribute
// `term` is String element in array, target of attributes
// `string` is complete NAString from data
// `atStr` is final
for term in butt2 {
var pos = NSRange(location: 0, length: string.length)
while true {
let next = string.rangeOfString(term, options: .LiteralSearch, range: pos)
if next.location == NSNotFound { break }
pos = NSRange(location: next.location+next.length, length: string.length-next.location-next.length)
atStr.addAttributes(butt2Attribs, range: next)
}
}
You don't have to resort to numerical ranges, but you do need to resort to a loop:
// atStr is mutable attributed string
// str is the input string
atStr.beginEditing()
var pos = NSRange(location: 0, length: atStr.length)
while true {
let next = atStr.rangeOfString(target, options: .LiteralSearch, range: pos)
if next.location == NSNotFound {
break
}
atStr.addAttributes(boldAttribs, range: next)
print(next)
pos = NSRange(location: next.location+next.length, length: atStr.length-next.location-next.length)
}
atStr.endEditing()

Checking if a word is real

I want to make sure that a word is real and this is my code:
var checker: UITextChecker = UITextChecker()
var range: NSRange = NSRange(location: 0,length: (count(completeWord)))
var misspelledRange: NSRange = checker.rangeOfMisspelledWordInString(completeWord, range: range, startingAt: 0, wrap: false, language: "en_US")
var isRealWord: Bool = misspelledRange.location == NSNotFound
if isRealWord {
println("Correct")
} else {
println("Not Correct")
}
But even if I give it a letter, it says correct. What can I do about that? Basically, I want to remove letters from the corrects.
UITextChecker gives you misspelled words in a sentence. It won't show you wrong letter in a word, but wrong word in the whole text input.
For example:
var checker: UITextChecker = UITextChecker()
let string:NSString = "Airplane is gren"
var range: NSRange = NSRange(location: 0,length: string.length)
var misspelledRange: NSRange = checker.rangeOfMisspelledWordInString(string as String, range: range, startingAt: 0, wrap: false, language: "en_US")
misspelledRange.toRange()
gives you result as 12..<16, i.e the whole word.
You could use guessesForWordRange to get possible correct substitutes:
let guesses = checker.guessesForWordRange(misspelledRange, inString: string as String, language: "en_US") as? [String]
(returns ["green", "greg", "grep", "grew", "grey", "gran", "grin", "glen", "aren", "oren", "wren"])