Split string by regrex and get matching range - swift

extension String {
func splitWithRegex(by regexStr: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: regexStr) else { return [] }
let nsRange = NSRange(startIndex..., in: self)
var index = startIndex
var array = regex.matches(in: self, range: nsRange)
.map { match -> String in
let range = Range(match.range, in: self)!
let result = self[index..<range.lowerBound]
index = range.upperBound
return String(result)
}
array.append(String(self[index...]))
return array
}
}
I use the above code to split a string on Swift using a regrex pattern and return an array of strings
How can I return an object of [(String, Range)] so I will use the split substrings as keys and it's range as the value.
For example:
let string = "This is a string"
let stringRange = string.splitWithRegex(by: "\\s+")
Result:
(This, 0...4)
(is, 5...7)
(a, 8...9)
(string, 10...16)

I guess that should do the trick, but I'm not totally sure:
func splitWithRegex2(by regexStr: String) -> [(String, Range<String.Index>)] {
guard let regex = try? NSRegularExpression(pattern: regexStr) else { return [] }
let nsRange = NSRange(startIndex..., in: self)
var index = startIndex
var array = regex.matches(in: self, range: nsRange)
.compactMap { match -> (String, Range<String.Index>)? in
guard let range = Range(match.range, in: self) else { return nil }
let resultRange = index..<range.lowerBound
let string = String(self[resultRange])
index = range.upperBound
guard !resultRange.isEmpty else { return nil } //This should fix the issue where the first match starts your string, in your case, an empty space
let values = (string, resultRange)
return values
}
//Append last value check: needed?
if index < endIndex {
array.append((String(self[index...]), index..<endIndex))
}
return array
}
To play:
let stringReg1 = "This is a string"
let stringRange1 = stringReg1.splitWithRegex2(by: "\\s+")
print(stringRange1)
stringRange1.forEach { //More userfriendly than Range(Swift.String.Index(_rawBits: 65536)..<Swift.String.Index(_rawBits: 327680)))
let nsRange = NSRange($0.1, in: stringReg1)
print("\($0.0), \(nsRange.location)...\(nsRange.location + nsRange.length)")
}
Output:
$>[("This", Range(Swift.String.Index(_rawBits: 1)..<Swift.String.Index(_rawBits: 262144))), ("is", Range(Swift.String.Index(_rawBits: 393216)..<Swift.String.Index(_rawBits: 524288))), ("a", Range(Swift.String.Index(_rawBits: 589824)..<Swift.String.Index(_rawBits: 655360))), ("string", Range(Swift.String.Index(_rawBits: 720896)..<Swift.String.Index(_rawBits: 1114113)))]
$>This, 0...4
$>is, 6...8
$>a, 9...10
$>string, 11...17
let stringReg2 = " This is a string "
let stringRange2 = stringReg2.splitWithRegex2(by: "\\s+")
print(stringRange2)
stringRange2.forEach { //More userfriendly than Range(Swift.String.Index(_rawBits: 65536)..<Swift.String.Index(_rawBits: 327680)))
let nsRange = NSRange($0.1, in: stringReg2)
print("\($0.0), \(nsRange.location)...\(nsRange.location + nsRange.length)")
}
Output:
$>[("This", Range(Swift.String.Index(_rawBits: 262144)..<Swift.String.Index(_rawBits: 524288))), ("is", Range(Swift.String.Index(_rawBits: 655360)..<Swift.String.Index(_rawBits: 786432))), ("a", Range(Swift.String.Index(_rawBits: 851968)..<Swift.String.Index(_rawBits: 917504))), ("string", Range(Swift.String.Index(_rawBits: 983040)..<Swift.String.Index(_rawBits: 1376256)))]
$>This, 4...8
$>is, 10...12
$>a, 13...14
$>string, 15...21

Related

How to extract Hashtags from text using SwiftUI?

Is there any way to find Hashtags from a text with SwiftUI?
This is how my try looks like:
calling the function like this : Text(convert(msg.findMentionText().joined(separator: " "), string: msg)).padding(.top, 8)
.
But it does not work at all.
My goal something like this:
extension String {
func findMentionText() -> [String] {
var arr_hasStrings:[String] = []
let regex = try? NSRegularExpression(pattern: "(#[a-zA-Z0-9_\\p{Arabic}\\p{N}]*)", options: [])
if let matches = regex?.matches(in: self, options:[], range:NSMakeRange(0, self.count)) {
for match in matches {
arr_hasStrings.append(NSString(string: self).substring(with: NSRange(location:match.range.location, length: match.range.length )))
}
}
return arr_hasStrings
}
}
func convert(_ hashElements:[String], string: String) -> NSAttributedString {
let hasAttribute = [NSAttributedString.Key.foregroundColor: UIColor.orange]
let normalAttribute = [NSAttributedString.Key.foregroundColor: UIColor.black]
let mainAttributedString = NSMutableAttributedString(string: string, attributes: normalAttribute)
let txtViewReviewText = string as NSString
hashElements.forEach { if string.contains($0) {
mainAttributedString.addAttributes(hasAttribute, range: txtViewReviewText.range(of: $0))
}
}
return mainAttributedString
}
You need to initailize Text() with a String, but instead you are attempting to initialize it with an Array of String.
You could either just display the first one if the array is not empty:
msg.findMentionText().first.map { Text($0) }
Or you could join the elements array into a single String:
Text(msg.findMentionText().joined(separator: " "))

Looking for the best way to filter text from string in Swift

I have an array of Strings in ascending sorted order. I want to filter char/text in that array and get results from searched text letter first and then the rest. I am looking for the simplest way to do this job.          
      
Example:
Var array = ["Anand", "Ani", "Dan", "Eion", "Harsh", "Jocab", "Roshan", "Stewart"]
and search text is "R"
Output should be:
Var outArray = ["Roshan", "Harsh", "Stewart"]
One way to do this is to first map the strings to a tuple containing the index of the search text in string, and the string itself. Then sort by the index, then map the tuples back to the strings.
let array = ["Anand", "Ani", "Dan", "Eion", "Harsh", "Jocab", "Roshan", "Stewart"]
let searchText = "R"
// compactMap acts as a filter, removing the strings where string.index(of: searchText, options: [.caseInsensitive]) returns nil
let result = array.compactMap { string in string.index(of: searchText, options: [.caseInsensitive]).map { ($0, string) } }
.sorted { $0.0 < $1.0 }.map { $0.1 }
The index(of:options:) method is taken from this answer here.
For Swift 4.x:
extension StringProtocol where Index == String.Index {
func index(of string: Self, options: String.CompareOptions = []) -> Index? {
return range(of: string, options: options)?.lowerBound
}
func endIndex(of string: Self, options: String.CompareOptions = []) -> Index? {
return range(of: string, options: options)?.upperBound
}
func indexes(of string: Self, options: String.CompareOptions = []) -> [Index] {
var result: [Index] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...].range(of: string, options: options) {
result.append(range.lowerBound)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
func ranges(of string: Self, options: String.CompareOptions = []) -> [Range<Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...].range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
Use filter on array to filter all the Strings that contain the searchText, i.e.
let array = ["Anand", "Ani", "Dan", "Eion", "Harsh", "Jocab", "Roshan", "Stewart"]
let searchText = "R"
let filteredArray = array.filter({$0.lowercased().contains(searchText.lowercased())})
let sortedArray = filteredArray.sorted { (str1, str2) -> Bool in
if let index1 = str1.lowercased().firstIndex(of: Character(searchText.lowercased())), let index2 = str2.lowercased().firstIndex(of: Character(searchText.lowercased())) {
return index1 < index2
}
return false
}
print(sortedArray) //["Roshan", "Harsh", "Stewart"]
let words = ["Anand", "Ani", "Dan", "Eion", "Harsh", "Jocab", "Roshan", "Stewart"]
let keyword = "r"
let result = words.filter { $0.contains(keyword) }
.sorted { ($0.hasPrefix(keyword) ? 0 : 1) < ($1.hasPrefix(keyword) ? 0 : 1) }

distance(from:to:)' is unavailable: Any String view index conversion can fail in Swift 4; please unwrap the optional indices

I was trying to migrate my app to Swift 4, Xcode 9. I get this error. Its coming from a 3rd party framework.
distance(from:to:)' is unavailable: Any String view index conversion can fail in Swift 4; please unwrap the optional indices
func nsRange(from range: Range<String.Index>) -> NSRange {
let utf16view = self.utf16
let from = range.lowerBound.samePosition(in: utf16view)
let to = range.upperBound.samePosition(in: utf16view)
return NSMakeRange(utf16view.distance(from: utf16view.startIndex, to: from), // Error: distance(from:to:)' is unavailable: Any String view index conversion can fail in Swift 4; please unwrap the optional indices
utf16view.distance(from: from, to: to))// Error: distance(from:to:)' is unavailable: Any String view index conversion can fail in Swift 4; please unwrap the optional indices
}
You can simply unwrap the optional indices like this:
func nsRange(from range: Range<String.Index>) -> NSRange? {
let utf16view = self.utf16
if let from = range.lowerBound.samePosition(in: utf16view), let to = range.upperBound.samePosition(in: utf16view) {
return NSMakeRange(utf16view.distance(from: utf16view.startIndex, to: from), utf16view.distance(from: from, to: to))
}
return nil
}
The error says that the distances you are generating are optionals and need to be unwrapped. Try this:
func nsRange(from range: Range<String.Index>) -> NSRange {
let utf16view = self.utf16
guard let lowerBound = utf16view.distance(from: utf16view.startIndex, to: from), let upperBound = utf16view.distance(from: from, to: to) else { return NSMakeRange(0, 0) }
return NSMakeRange(lowerBound, upperBound)
}
However the return could be handled better in the guard statement. I'd recommend making the return type of the function NSRange? and checking for nil wherever you call the function to avoid inaccurate values being returned.
Please check :
let dogString = "Dog‼🐶"
let range = dogString.range(of: "🐶")!
// This is using Range
let strRange = dogString.range(range: range)
print((dogString as NSString).substring(with: strRange!)) // 🐶
extension String {
func range(range : Range<String.Index>) -> NSRange? {
let utf16view = self.utf16
guard
let from = String.UTF16View.Index(range.lowerBound, within: utf16view),
let to = String.UTF16View.Index(range.upperBound, within: utf16view)
else { return nil }
let utf16Offset = utf16view.startIndex.encodedOffset
let toOffset = to.encodedOffset
let fromOffset = from.encodedOffset
return NSMakeRange(fromOffset - utf16Offset, toOffset - fromOffset)
}
}
// This is using NSRange
let strNSRange = dogString.range(nsRange: NSRange(range, in: dogString))
print((dogString as NSString).substring(with: strNSRange!)) // 🐶
extension String {
func range(nsRange: NSRange) -> NSRange? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
let from = from16.samePosition(in: self),
let to = to16.samePosition(in: self)
else { return nil }
return NSMakeRange(from.encodedOffset, to.encodedOffset)
}
}

Hex string to text conversion - swift 3

I'm trying to convert an hex string to text.
This is what i have:
// Str to Hex
func strToHex(text: String) -> String {
let hexString = text.data(using: .utf8)!.map{ String(format:"%02x", $0) }.joined()
return "0x" + hexString
}
and I'm trying to reverse the hex string that I've just created back to the original one.
So, for example:
let foo: String = strToHex(text: "K8") //output: "0x4b38"
and i would like to do something like
let bar: String = hexToStr(hex: "0x4b38") //output: "K8"
can someone help me?
Thank you
You probably can use something like this:
func hexToStr(text: String) -> String {
let regex = try! NSRegularExpression(pattern: "(0x)?([0-9A-Fa-f]{2})", options: .caseInsensitive)
let textNS = text as NSString
let matchesArray = regex.matches(in: textNS as String, options: [], range: NSMakeRange(0, textNS.length))
let characters = matchesArray.map {
Character(UnicodeScalar(UInt32(textNS.substring(with: $0.rangeAt(2)), radix: 16)!)!)
}
return String(characters)
}
NSRegularExpression is overkill for the job. You can convert the string to byte array by grabbing two characters at a time:
func hexToString(hex: String) -> String? {
guard hex.characters.count % 2 == 0 else {
return nil
}
var bytes = [CChar]()
var startIndex = hex.index(hex.startIndex, offsetBy: 2)
while startIndex < hex.endIndex {
let endIndex = hex.index(startIndex, offsetBy: 2)
let substr = hex[startIndex..<endIndex]
if let byte = Int8(substr, radix: 16) {
bytes.append(byte)
} else {
return nil
}
startIndex = endIndex
}
bytes.append(0)
return String(cString: bytes)
}
Solution that supports also special chars like emojis etc.
static func hexToPlain(input: String) -> String {
let pairs = toPairsOfChars(pairs: [], string: input)
let bytes = pairs.map { UInt8($0, radix: 16)! }
let data = Data(bytes)
return String(bytes: data, encoding: .utf8)!
}

range function and crash in swift 3

My below code crashes:
func getrange(_ from: Int, length: Int) -> Range<String.Index>? {
guard let fromU16 = utf16.index(utf16.startIndex, offsetBy: from, limitedBy: utf16.endIndex), fromU16 != utf16.endIndex else {
return nil ----->crashes here
}
let toU16 = utf16.index(fromU16, offsetBy: length, limitedBy: utf16.endIndex) ?? utf16.endIndex
guard let from = String.Index(fromU16, within: self),
let to = String.Index(toU16, within: self) else { return nil }
return from ..< to
}
This code is crashing with swift 3 migration.
Can someone help debugging the issue.
Below is the sequence of events:
//input for below function is: text “123456789”, string “0”, nsrange = location =9, length=0
1) function 1
static func numericText(_ text: String, replacedBy string: String, in nsrange: NSRange) -> String {
guard let range = text.range(for: nsrange) else {
//assertionFailure("Should never reach here")
return text.numericString()
}
// Apply Replacement String to the textField text and extract only the numeric values
return text.replacingCharacters(in: range, with: string)
.numericString()
}
2) function 2
func range(for nsrange: NSRange) -> Range<String.Index>? {
return range(nsrange.location, length: nsrange.length)
}
3) function 3
func range(_ from: Int, length: Int) -> Range<String.Index>? {
guard let fromU16 = utf16.index(utf16.startIndex, offsetBy: from, limitedBy: utf16.endIndex), fromU16 != utf16.endIndex else {
return nil
}
let toU16 = utf16.index(fromU16, offsetBy: length, limitedBy: utf16.endIndex) ?? utf16.endIndex
guard let from = String.Index(fromU16, within: self),
let to = String.Index(toU16, within: self) else { return nil }
return from ..< to
}
Sorry, I didn't update during the weekend.
I reviewed your question.
I can implement your function 1:
extension String {
func getrange(_ from: Int, length: Int) -> Range<String.Index>? {
guard let fromU16 = utf16.index(utf16.startIndex, offsetBy: from, limitedBy: utf16.endIndex), fromU16 != utf16.endIndex else {
return nil
}
let toU16 = utf16.index(fromU16, offsetBy: length, limitedBy: utf16.endIndex) ?? utf16.endIndex
guard let from = String.Index(fromU16, within: self),
let to = String.Index(toU16, within: self) else { return nil }
return from ..< to
}
}
But I cant implement your function 2, is that converted to Swift3 syntax yet?
My question is this,
Below is the sequence of events:
//input for below function is: text “123456789”, string “0”, nsrange = location =9, length=0
your input, your location shouldn't be 9. As your string length is 9, the max location your can replace should be 8?
Just replacing utf with unicodeScalars in the code fixed the issue.