I want to slice a very long string from one word to another. I want to get the substring between those words.
For that, I use the following string extension:
extension String {
func slice(from: String, to: String) -> String? {
guard let rangeFrom = range(of: from)?.upperBound else { return nil }
guard let rangeTo = self[rangeFrom...].range(of: to)?.lowerBound else { return nil }
return String(self[rangeFrom..<rangeTo])
}
That works really good, but my raw-string contains a few of the "from" "to"-words and I need every substring that is between of these two words, but with my extension I can ony get the first substring.
Example:
let raw = "id:244476end36475677id:383848448end334566777788id:55678900end543"
I want to get the following substrings from this raw string example:
sub1 = "244476"
sub2 = "383848448"
sub3 = "55678900"
If I call:
var text = raw.slice(from: "id:" , to: "end")
I only get the first occurence (text = "244476")
Thank you for reading. Every answer would be nice.
PS: I get always an error by making code snippets in stackoverflow.
You can get the ranges of your substrings using a while loop to repeat the search from that point to the end of your string and use map to get the substrings from the resulting ranges:
extension StringProtocol {
func ranges<S:StringProtocol,T:StringProtocol>(between start: S, and end: T, options: String.CompareOptions = []) -> [Range<Index>] {
var ranges: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let lower = self[startIndex...].range(of: start, options: options)?.upperBound,
let range = self[lower...].range(of: end, options: options) {
let upper = range.lowerBound
ranges.append(lower..<upper)
startIndex = range.upperBound
}
return ranges
}
func substrings<S:StringProtocol,T:StringProtocol>(between start: S, and end: T, options: String.CompareOptions = []) -> [SubSequence] {
ranges(between: start, and: end, options: options).map{self[$0]}
}
}
Playground testing:
let string = """
your text
id:244476end
id:383848448end
id:55678900end
the end
"""
let substrings = string.substrings(between: "id:", and: "end") // ["244476", "383848448", "55678900"]
Rather thant trying to parse the string from start to end, I would use a combination of existing methods to transform it into the desire result. Here's How I would do this:
import Foundation
let raw = "id:244476end36475677id:383848448end334566777788id:55678900end543"
let result = raw
.components(separatedBy: "id:")
.filter{ !$0.isEmpty }
.map { segment -> String in
let slices = segment.components(separatedBy: "end")
return slices.first! // Removes the `end` and everything thereafter
}
print(result) // => ["244476", "383848448", "55678900"]
Related
I'm programming in swift 5 a search routine and I want to highlight in a string if the search is contained in this string (e.g. if I search for "bcd" in a string like "äbcdef" the result should look like "äbcdef". In doing so I wrote an extension for String to split a String into the substring before the match with the search string (="before") , the match with the search string (="match") and the substring afterwards (="after").
extension String {
func findSubstring(forSearchStr search: String, caseSensitive sensitive: Bool) -> (before: String, match: String, after: String) {
var before = self
var searchStr = search
if !sensitive {
before = before.lowercased()
searchStr = searchStr.lowercased()
}
var match = ""
var after = ""
let totalStringlength = before.count
let searchStringlength = searchStr.count
var startpos = self.startIndex
var endpos = self.endIndex
for id in 0 ... (totalStringlength - searchStringlength) {
startpos = self.index(self.startIndex, offsetBy: id)
endpos = self.index(startpos, offsetBy: searchStringlength)
if searchStr == String(before[startpos ..< endpos]) {
before = String(self[self.startIndex ..< startpos])
match = String(self[startpos ..< endpos])
if id < totalStringlength - searchStringlength - 1 {
startpos = self.index(startpos, offsetBy: searchStringlength)
after = String(self[startpos ..< self.endIndex])
}
break
}
}
return (before, match, after)
} // end findSubstring()
}
My problem is, that this routine works well for all strings without special characters like the German Umlaute "ä, ö, ü" or "ß". If a string contains one of these characters the returned substrings "match" and "after" are shifted one sign to the right. In the example above the result for the search "bcd" is in this case "äbcdef"
My question is, what do I have to do to handle this characters properly as well?
By the way: is there a simplier solution than mine to split a string as described than what I have programmed (which seems to me to be rather complex :) )
Thanks for your valuable support
String comparison is a complicated issue, and is something you would not want to handle yourself unless you are studying this.
Just use String.range(of:options:):
extension String {
func findSubstring(forSearchStr search: String, caseSensitive sensitive: Bool) -> (before: String, match: String, after: String)? {
if let substringRange = range(of: search, options: sensitive ? [] : [.caseInsensitive], locale: nil) {
return (String(self[startIndex..<substringRange.lowerBound]),
String(self[substringRange]),
String(self[substringRange.upperBound..<self.endIndex]))
} else {
return nil
}
}
}
// (before: "ä", match: "bcd", after: "e")
print("äbcde".findSubstring(forSearchStr: "bcd", caseSensitive: true)!)
Note that this is not a literal comparison. For example:
// prints (before: "", match: "ß", after: "")
print("ß".findSubstring(forSearchStr: "ss", caseSensitive: false)!)
If you want a literal comparison, use the literal option:
range(of: search, options: sensitive ? [.literal] : [.caseInsensitive, .literal])
I am attempting to use rangeOfCharacter to create an app, but am unable to understand its documentation:
func rangeOfCharacter(from: CharacterSet, options:
String.CompareOptions, range: Range<String.Index>?)
-Finds and returns the range in the String of the first character from
a given character set found in a given range with given options.
Documentation link: https://developer.apple.com/documentation/swift/string#symbols
I am working on an exercise to create a function which will take in a name and return the name, minus any consonants before the first vowel. The name should be returned unchanged if there are no consonants before the first vowel.
Below is the code I have so far:
func shortNameFromName(name: String) -> String {
var shortName = name.lowercased()
let vowels = "aeiou"
let vowelRange = CharacterSet(charactersIn: vowels)
rangeOfCharacter(from: vowels, options: shortName,
range: substring(from: shortName[0]))
Any help is much appreciated. Apologies for the newbie mistakes.
I hate Swift ranges. But hopefully things will get better with Swift 4.
let name = "Michael"
var shortName = name.lowercased()
let vowels = "aeiou"
let vowelSet = CharacterSet(charactersIn: vowels)
let stringSet = shortName
if let range = stringSet.rangeOfCharacter(from: vowelSet, options: String.CompareOptions.caseInsensitive)
{
let startIndex = range.lowerBound
let substring = name.substring(from: range.lowerBound)
print(substring)
}
Use this code with a regular expression your problem is solved
Improved
func shortNameFromName(name: String) -> String {
do{
let regex2 = try NSRegularExpression(pattern: "[a|e|i|o|u].*", options:[.dotMatchesLineSeparators])
if let result = regex2.firstMatch(in: name.lowercased(), options: .init(rawValue: 0), range: NSRange(location: 0, length: NSString(string: name).length))
{
return String(NSString(string: name).substring(with: result.range))
}
}
catch
{
debugPrint(error.localizedDescription)
}
return ""
}
Tested
debugPrint(self.shortNameFromName(name: "yhcasid")) //test1
debugPrint(self.shortNameFromName(name: "ayhcasid")) //test2
debugPrint(self.shortNameFromName(name: "😀abc")) // working thanks to #MartinR
Console Log
test1 result
"asid"
test2 result
"ayhcasid"
test3 result
"abc"
Hope this helps
You are passing completely wrong arguments to the method.
rangeOfCharacter accepts 3 arguments. You passed in the character set correctly, but the last two arguments you passed makes no sense. You should pass a bunch of options as the second argument, instead you passed in a string. The third argument is supposed to be a Range but you passed the return value of a substring call.
I think rangeOfCharacter isn't suitable here. There are lots more better ways to do this. For example:
func shortNameFromName(name: String) -> String {
return String(name.characters.drop(while: {!"aeiou".characters.contains($0)}))
}
Swift 3
replace your code here..
func shortNameFromName(name: String) -> String {
var shortName = name.lowercased()
let newstring = shortName
let vowels: [Character] = ["a","e","i","o","u"]
for i in shortName.lowercased().characters {
if vowels.contains(i) {
break
}
else {
shortName = shortName.replacingOccurrences(of: "\(i)", with: "")
}
}
if shortName != "" {
return shortName
}
else
{
return newstring
}
I cannot think of the a function to remove a repeating substring from my string. My string looks like this:
"<bold><bold>Rutger</bold> Roger</bold> rented a <bold>testitem zero dollars</bold> from <bold>Rutger</bold>."
And if <bold> is followed by another <bold> I want to remove the second <bold>. When removing that second <bold> I also want to remove the first </bold> that follows.
So the output that I'm looking for should be this:
"<bold>Rutger Roger</bold> rented a <bold>testitem zero dollars</bold> from <bold>Rutger</bold>."
Anyone know how to achieve this in Swift (2.2)?
I wrote a solution using regex with the assumption that tags won't appear in nested contents more than 1 times. In other words it just cleans the double tags not more than that. You can use the same code and a recursive call to clean as many nested repeating tag as you want:
class Cleaner {
var tags:Array<String> = [];
init(tags:Array<String>) {
self.tags = tags;
}
func cleanString(html:String) -> String {
var res = html
do {
for tag in tags {
let start = "<\(tag)>"
let end = "</\(tag)>"
let pattern = "\(start)(.*?)\(end)"
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
let matches = regex.matches(in: res, options: [], range: NSRange(location: 0, length: res.utf16.count))
var diff = 0;
for match in matches {
let outer_range = NSMakeRange(match.rangeAt(0).location - diff, match.rangeAt(0).length)
let inner_range = NSMakeRange(match.rangeAt(1).location - diff, match.rangeAt(1).length)
let node = (res as NSString).substring(with: outer_range)
let content = (res as NSString).substring(with: inner_range)
// look for the starting tag in the content of the node
if content.range(of: start) != nil {
res = (res as NSString).replacingCharacters(in: outer_range, with: content);
//for shifting future ranges
diff += (node.utf16.count - content.utf16.count)
}
}
}
}
catch {
print("regex was bad!")
}
return res
}
}
let cleaner = Cleaner(tags: ["bold"]);
let html = "<bold><bold>Rutger</bold> Roger</bold> rented a <bold><bold>testitem</bold> zero dollars</bold> from <bold>Rutger</bold>."
let cleaned = cleaner.cleanString(html: html)
print(cleaned)
//<bold>Rutger Roger</bold> rented a <bold>testitem zero dollars</bold> from <bold>Rutger</bold>.
Try this, i have just made. Hope this helpful.
class Test : NSObject {
static func removeFirstString (originString: String, removeString: String, withString: String) -> String {
var genString = originString
if originString.contains(removeString) {
let range = originString.range(of: removeString)
genString = genString.replacingOccurrences(of: removeString, with: withString, options: String.CompareOptions.anchored, range: range)
}
return genString
}
}
var newString = Test.removeFirstString(originString: str, removeString: "<bold>", withString: "")
newString = Test.removeFirstString(originString: newString, removeString: "</bold>", withString: "")
I have a string composed of words, some of which contain punctuation, which I would like to remove, but I have been unable to figure out how to do this.
For example if I have something like
var words = "Hello, this : is .. a string?"
I would like to be able to create an array with
"[Hello, this, is, a, string]"
My original thought was to use something like words.stringByTrimmingCharactersInSet() to remove any characters I didn't want but that would only take characters off the ends.
I thought maybe I could iterate through the string with something in the vein of
for letter in words {
if NSCharacterSet.punctuationCharacterSet.characterIsMember(letter){
//remove that character from the string
}
}
but I'm unsure how to remove the character from the string. I'm sure there are some problems with the way that if statement is set up, as well, but it shows my thought process.
Xcode 11.4 • Swift 5.2 or later
extension StringProtocol {
var words: [SubSequence] {
split(whereSeparator: \.isLetter.negation)
}
}
extension Bool {
var negation: Bool { !self }
}
let sentence = "Hello, this : is .. a string?"
let words = sentence.words // ["Hello", "this", "is", "a", "string"]
String has a enumerateSubstringsInRange() method.
With the .ByWords option, it detects word boundaries and
punctuation automatically:
Swift 3/4:
let string = "Hello, this : is .. a \"string\"!"
var words : [String] = []
string.enumerateSubstrings(in: string.startIndex..<string.endIndex,
options: .byWords) {
(substring, _, _, _) -> () in
words.append(substring!)
}
print(words) // [Hello, this, is, a, string]
Swift 2:
let string = "Hello, this : is .. a \"string\"!"
var words : [String] = []
string.enumerateSubstringsInRange(string.characters.indices,
options: .ByWords) {
(substring, _, _, _) -> () in
words.append(substring!)
}
print(words) // [Hello, this, is, a, string]
This works with Xcode 8.1, Swift 3:
First define general-purpose extension for filtering by CharacterSet:
extension String {
func removingCharacters(inCharacterSet forbiddenCharacters:CharacterSet) -> String
{
var filteredString = self
while true {
if let forbiddenCharRange = filteredString.rangeOfCharacter(from: forbiddenCharacters) {
filteredString.removeSubrange(forbiddenCharRange)
}
else {
break
}
}
return filteredString
}
}
Then filter using punctuation:
let s:String = "Hello, world!"
s.removingCharacters(inCharacterSet: CharacterSet.punctuationCharacters) // => "Hello world"
let charactersToRemove = NSCharacterSet.punctuationCharacterSet().invertedSet
let aWord = "".join(words.componentsSeparatedByCharactersInSet(charactersToRemove))
An alternate way to filter characters from a set and obtain an array of words is by using the array's filter and reduce methods. It's not as compact as other answers, but it shows how the same result can be obtained in a different way.
First define an array of the characters to remove:
let charactersToRemove = Set(Array(".:?,"))
next convert the input string into an array of characters:
let arrayOfChars = Array(words)
Now we can use reduce to build a string, obtained by appending the elements from arrayOfChars, but skipping all the ones included in charactersToRemove:
let filteredString = arrayOfChars.reduce("") {
let str = String($1)
return $0 + (charactersToRemove.contains($1) ? "" : str)
}
This produces a string without the punctuation characters (as defined in charactersToRemove).
The last 2 steps:
split the string into an array of words, using the blank character as separator:
let arrayOfWords = filteredString.componentsSeparatedByString(" ")
last, remove all empty elements:
let finalArrayOfWords = arrayOfWords.filter { $0.isEmpty == false }
NSScaner way:
let words = "Hello, this : is .. a string?"
//
let scanner = NSScanner(string: words)
var wordArray:[String] = []
var word:NSString? = ""
while(!scanner.atEnd) {
var sr = scanner.scanCharactersFromSet(NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ"), intoString: &word)
if !sr {
scanner.scanLocation++
continue
}
wordArray.append(String(word!))
}
println(wordArray)
I get data (HTML string) from website. I want to extract all links. I write function (it works), but it is so slow...
Can you help me to optimize it? What standard functions I can use?
Function logic: find "http:.//" sting in text, and then read string (buy char) until I will not get "\"".
extension String {
subscript (i: Int) -> Character {
return self[advance(self.startIndex, i)]
}
subscript (i: Int) -> String {
return String(self[i] as Character)
}
subscript (r: Range<Int>) -> String {
return substringWithRange(Range(start: advance(startIndex, r.startIndex), end: advance(startIndex, r.endIndex)))
}}
func extractAllLinks(text:String) -> Array<String>{
var stringArray = Array<String>()
var find = "http://" as String
for (var i = countElements(find); i<countElements(text); i++)
{
var ch:Character = text[i - Int(countElements(find))]
if (ch == find[0])
{
var j = 0
while (ch == find[j])
{
var ch2:Character = find[j]
if(countElements(find)-1 == j)
{
break
}
j++
i++
ch = text[i - Int(countElements(find))]
}
i -= j
if (j == (countElements(find)-1))
{
var str = ""
for (; text[i - Int(countElements(find))] != "\""; i++)
{
str += text[i - Int(countElements(find))]
}
stringArray.append(str)
}
}
}
return stringArray}
Like AdamPro13 said above using NSDataDetector you can easily get all the URLs, see it the following code :
let text = "http://www.google.com. http://www.bla.com"
let types: NSTextCheckingType = .Link
var error : NSError?
let detector = NSDataDetector(types: types.rawValue, error: &error)
var matches = detector!.matchesInString(text, options: nil, range: NSMakeRange(0, count(text)))
for match in matches {
println(match.URL!)
}
It outputs :
http://www.google.com
http://www.bla.com
Updated to Swift 2.0
let text = "http://www.google.com. http://www.bla.com"
let types: NSTextCheckingType = .Link
let detector = try? NSDataDetector(types: types.rawValue)
guard let detect = detector else {
return
}
let matches = detect.matchesInString(text, options: .ReportCompletion, range: NSMakeRange(0, text.characters.count))
for match in matches {
print(match.URL!)
}
Remember to use the guard statement in the above case it must be inside a function or loop.
I hope this help.
And that is the answer for Swift 5.0
let text = "http://www.google.com. http://www.bla.com"
func checkForUrls(text: String) -> [URL] {
let types: NSTextCheckingResult.CheckingType = .link
do {
let detector = try NSDataDetector(types: types.rawValue)
let matches = detector.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, text.count))
return matches.compactMap({$0.url})
} catch let error {
debugPrint(error.localizedDescription)
}
return []
}
checkForUrls(text: text)
Details
Swift 5.2, Xcode 11.4 (11E146)
Solution
// MARK: DataDetector
class DataDetector {
private class func _find(all type: NSTextCheckingResult.CheckingType,
in string: String, iterationClosure: (String) -> Bool) {
guard let detector = try? NSDataDetector(types: type.rawValue) else { return }
let range = NSRange(string.startIndex ..< string.endIndex, in: string)
let matches = detector.matches(in: string, options: [], range: range)
loop: for match in matches {
for i in 0 ..< match.numberOfRanges {
let nsrange = match.range(at: i)
let startIndex = string.index(string.startIndex, offsetBy: nsrange.lowerBound)
let endIndex = string.index(string.startIndex, offsetBy: nsrange.upperBound)
let range = startIndex..<endIndex
guard iterationClosure(String(string[range])) else { break loop }
}
}
}
class func find(all type: NSTextCheckingResult.CheckingType, in string: String) -> [String] {
var results = [String]()
_find(all: type, in: string) {
results.append($0)
return true
}
return results
}
class func first(type: NSTextCheckingResult.CheckingType, in string: String) -> String? {
var result: String?
_find(all: type, in: string) {
result = $0
return false
}
return result
}
}
// MARK: String extension
extension String {
var detectedLinks: [String] { DataDetector.find(all: .link, in: self) }
var detectedFirstLink: String? { DataDetector.first(type: .link, in: self) }
var detectedURLs: [URL] { detectedLinks.compactMap { URL(string: $0) } }
var detectedFirstURL: URL? {
guard let urlString = detectedFirstLink else { return nil }
return URL(string: urlString)
}
}
Usage
let text = """
Lorm Ipsum is simply dummy text of the printing and typesetting industry. apple.com/ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. http://gooogle.com. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. yahoo.com It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
"""
print(text.detectedLinks)
print(text.detectedFirstLink)
print(text.detectedURLs)
print(text.detectedFirstURL)
Console output
["apple.com/", "http://gooogle.com", "yahoo.com"]
Optional("apple.com/")
[apple.com/, http://gooogle.com, yahoo.com]
Optional(apple.com/)
Very helpful thread! Here's an example that worked in Swift 1.2, based on Victor Sigler's answer.
// extract first link (if available) and open it!
let text = "How technology is changing our relationships to each other: http://t.ted.com/mzRtRfX"
let types: NSTextCheckingType = .Link
do {
let detector = try NSDataDetector(types: types.rawValue)
let matches = detector.matchesInString(text, options: .ReportCompletion, range: NSMakeRange(0, text.characters.count))
if matches.count > 0 {
let url = matches[0].URL!
print("Opening URL: \(url)")
UIApplication.sharedApplication().openURL(url)
}
} catch {
// none found or some other issue
print ("error in findAndOpenURL detector")
}
There's actually a class called NSDataDetector that will detect the link for you.
You can find an example of it on NSHipster here: http://nshipster.com/nsdatadetector/
I wonder if you realise that every single time that you call countElements, a major complex function is called that has to scan all the Unicode characters in your string, and extract extended grapheme clusters from them and count them. If you don't know what an extended grapheme cluster is then you should be able to imagine that this isn't cheap and major overkill.
Just convert it to an NSString*, call rangeOfString and be done with it.
Obviously what you do is totally unsafe, because http:// doesn't mean there is a link. You can't just look for strings in html and hope it works; it doesn't. And then there is https, Http, hTtp, htTp, httP and so on and so on and so on. But that's all easy, for the real horror follow the link in Uttam Sinha's comment.
As others have pointed out, you are better off using regexes, data detectors or a parsing library. However, as specific feedback on your string processing:
The key with Swift strings is to embrace the forward-only nature of them. More often than not, integer indexing and random access is not necessary. As #gnasher729 pointed out, every time you call count you are iterating over the string. Similarly, the integer indexing extensions are linear, so if you use them in a loop, you can easily accidentally create a quadratic or cubic-complexity algorithm.
But in this case, there's no need to do all that work to convert string indices to random-access integers. Here is a version that I think is performing similar logic (look for a prefix, then look from there for a " character - ignoring that this doesn't cater for https, upper/lower case etc) using only native string indices:
func extractAllLinks(text: String) -> [String] {
var links: [String] = []
let prefix = "http://"
let prefixLen = count(prefix)
for var idx = text.startIndex; idx != text.endIndex; ++idx {
let candidate = text[idx..<text.endIndex]
if candidate.hasPrefix(prefix),
let closingQuote = find(candidate, "\"") {
let link = candidate[candidate.startIndex..<closingQuote]
links.append(link)
idx = advance(idx, count(link))
}
}
return links
}
let text = "This contains the link \"http://www.whatever.com/\" and"
+ " the link \"http://google.com\""
extractAllLinks(text)
Even this could be further optimized (the advance(idx, count()) is a little inefficient) if there were other helpers such as findFromIndex etc. or a willingness to do without string slices and hand-roll the search for the end character.