How to get the range of the first line in a string? - swift

I would like to change the formatting of the first line of text in an NSTextView (give it a different font size and weight to make it look like a headline). Therefore, I need the range of the first line. One way to go is this:
guard let firstLineString = textView.string.components(separatedBy: .newlines).first else {
return
}
let range = NSRange(location: 0, length: firstLineString.count)
However, I might be working with quite long texts so it appears to be inefficient to first split the entire string into line components when all I need is the first line component. Thus, it seems to make sense to use the firstIndex(where:) method:
let firstNewLineIndex = textView.string.firstIndex { character -> Bool in
return CharacterSet.newlines.contains(character)
}
// Then: Create an NSRange from 0 up to firstNewLineIndex.
This doesn't work and I get an error:
Cannot convert value of type '(Unicode.Scalar) -> Bool' to expected argument type 'Character'
because the contains method accepts not a Character but a Unicode.Scalar as a parameter (which doesn't really make sense to me because then it should be called a UnicodeScalarSet and not a CharacterSet, but nevermind...).
My question is:
How can I implement this in an efficient way, without first slicing the whole string?
(It doesn't necessarily have to use the firstIndex(where:) method, but appears to be the way to go.)

A String.Index range for the first line in string can be obtained with
let range = string.lineRange(for: ..<string.startIndex)
If you need that as an NSRange then
let nsRange = NSRange(range, in: string)
does the trick.

You can use rangeOfCharacter, which returns the Range<String.Index> of the first character from a set in your string:
extension StringProtocol where Index == String.Index {
var partialRangeOfFirstLine: PartialRangeUpTo<String.Index> {
return ..<(rangeOfCharacter(from: .newlines)?.lowerBound ?? endIndex)
}
var rangeOfFirstLine: Range<Index> {
return startIndex..<partialRangeOfFirstLine.upperBound
}
var firstLine: SubSequence {
return self[partialRangeOfFirstLine]
}
}
You can use it like so:
var str = """
some string
with new lines
"""
var attributedString = NSMutableAttributedString(string: str)
let firstLine = NSAttributedString(string: String(str.firstLine))
// change firstLine as you wish
let range = NSRange(str.rangeOfFirstLine, in: str)
attributedString.replaceCharacters(in: range, with: firstLine)

Related

Split String or Substring with Regex pattern in Swift

First let me point out... I want to split a String or Substring with any character that is not an alphabet, a number, # or #. That means, I want to split with whitespaces(spaces & line breaks) and special characters or symbols excluding # and #
In Android Java, I am able to achieve this with:
String[] textArr = text.split("[^\\w_##]");
Now, I want to do the same in Swift. I added an extension to String and Substring classes
extension String {}
extension Substring {}
In both extensions, I added a method that returns an array of Substring
func splitWithRegex(by regexStr: String) -> [Substring] {
//let string = self (for String extension) | String(self) (for Substring extension)
let regex = try! NSRegularExpression(pattern: regexStr)
let range = NSRange(string.startIndex..., in: string)
return regex.matches(in: string, options: .anchored, range: range)
.map { match -> Substring in
let range = Range(match.range(at: 1), in: string)!
return string[range]
}
}
And when I tried to use it, (Only tested with a Substring, but I also think String will give me the same result)
let textArray = substring.splitWithRegex(by: "[^\\w_##]")
print("substring: \(substring)")
print("textArray: \(textArray)")
This is the out put:
substring: This,is a #random #text written for debugging
textArray: []
Please can Someone help me. I don't know if the problem if from my regex [^\\w_##] or from splitWithRegex method
The main reason why the code doesn't work is range(at: 1) which returns the content of the first captured group, but the pattern does not capture anything.
With just range the regex returns the ranges of the found matches, but I suppose you want the characters between.
To accomplish that you need a dynamic index starting at the first character. In the map closure return the string from the current index to the lowerBound of the found range and set the index to its upperBound. Finally you have to add manually the string from the upperBound of the last match to the end.
The Substring type is a helper type for slicing strings. It should not be used beyond a temporary scope.
extension String {
func splitWithRegex(by regexStr: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: regexStr) else { return [] }
let range = NSRange(startIndex..., in: self)
var index = startIndex
var array = regex.matches(in: self, range: range)
.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
}
}
let text = "This,is a #random #text written for debugging"
let textArray = text.splitWithRegex(by: "[^\\w_##]")
print(textArray) // ["This", "is", "a", "#random", "#text", "written", "for", "debugging"]
However in macOS 13 and iOS 16 there is a new API quite similar to the java API
let text = "This,is a #random #text written for debugging"
let textArray = Array(text.split(separator: /[^\w_##]/))
print(textArray)
The forward slashes indicate a regex literal

Format String left of multiple characters in Swift 5?

I have some Strings that vary in length but always end in "listing(number)"
myString = 9AMnep8MAziUCK7VwKF51mXZ2listing28
.
I want to get the String without "listing(number)":
9AMnep8MAziUCK7VwKF51mXZ2
.
Methods I've tried such as .index(of: ) only let you format based off one character. Any simple solutions?
A possible solution is to search for the substring with Regular Expression and remove the result (replace it with empty string)
let myString = "9AMnep8MAziUCK7VwKF51mXZ2listing28"
let trimmedString = myString.replacingOccurrences(of: "listing\\d+$", with: "", options: .regularExpression)
\\d+ searches for one ore more digits
$ represents the end of the string
Alternatively without creating a new string
var myString = "9AMnep8MAziUCK7VwKF51mXZ2listing28"
if let range = myString.range(of: "listing\\d+$", options: .regularExpression) {
myString.removeSubrange(range)
}
Another option is to split the string in parts with "listing" as separator
let result = myString.components(separatedBy: "listing").first
So to solve your issue find the code below with few comments written to try and explain each steps have taken. kindly note i have modified or arrived at this solution using this links as a guide.
https://stackoverflow.com/a/40070835/6596443
https://www.dotnetperls.com/substring-swift
extension String {
//
// Paramter inputString: This is the string you want to manipulate
// Paramter- startStringOfUnwanted: This is the string you want to start the removal or replacement from
//return : The expected output you want but can be emptystring if unable to
static func trimUnWantedEndingString(inputString: String,startStringOfUnwanted: String) -> String{
//Output string
var outputString: String?
//Getting the range based on the string content
if let range = myString.range(of: startStringOfUnwanted) {
//Get the lowerbound of the range
let lower = range.lowerBound
//Get the upperbound of the range
let upper = range.upperBound
//Get the integer position of the start index of the unwanted string i added plus one to ensure it starts from the right position
let startPos = Int(myString.distance(from: myString.startIndex, to: lower))+1
//Get the integer position of the end index of the unwanted string i added plus one to ensure it starts from the right position
let endPos = Int(myString.distance(from: myString.startIndex, to: upper))+1
//Substract the start int from the end int to get the integer value that will be used to get the last string i want to stop trimming at
let endOffsetBy = endPos-startPos
//get thes string char ranges of values
let result = myString.index(myString.startIndex, offsetBy: 0)..<myString.index(myString.endIndex, offsetBy: -endOffsetBy)
//converts the results to string or get the string representation of the result and then assign it to the OutputString
outputString = String(myString[result]);
}
return outputString ?? "";
}
}
let myString = "9AMnep8MAziUCK7VwKF51mXZ2listing28"
String.trimUnWantedEndingString(inputString: myString, startStringOfUnwanted:"listing")

Range<String.Index> Versus String.Index

I am having an issue understanding the difference between Range-String.Index- and String.Index
For example:
func returnHtmlContent() -> String {
let filePath = URL(string:"xxx.htm")
filePath?.startAccessingSecurityScopedResource();
let htmlFile = Bundle.main.path(forResource: "getData", ofType: "htm");
let html = try! String(contentsOfFile: htmlFile!, encoding: String.Encoding.utf8);
filePath?.stopAccessingSecurityScopedResource();
return html;
};
func refactorHtml(content: String) -> String {
let StartingString = "<div id=\"1\">";
let EndingString = "</div></form>";
func selectString() -> String {
var htmlContent = returnHtmlContent();
let charLocationStart = htmlContent.range(of: StartingString);
let charLocationEnd = htmlContent.range(of: EndingString);
htmlContent.remove(at: charLocationStart);
return htmlContent;
};
let formattedBody = selectString();
return formattedBody;
};
refactorHtml(content: returnHtmlContent());
The idea in pseudocode
Generated HTMLBody
Pass To Function that formats
Remove all characters before StartingString
Remove all Characters After EndingString
Send NewString to Variable
Now - when I try to find the index position I cant seem to get the right value type, this is the error I am getting
Cannot convert value of type 'Range<String.Index>?' to expected argument type 'String.Index'
This is running in a playground
String indices aren't integers. They're opaque objects (of type String.Index) which can be used to subscript into a String to obtain a character.
Ranges aren't limited to only Range<Int>. If you look at the declaration of Range, you can see it's generic over any Bound, so long as the Bound is Comparable (which String.Index is).
So a Range<String.Index> is just that. It's a range of string indices, and just like any other range, it has a lowerBound, and an upperBound.

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 use replacingOccurrences for NSAttributedString in swift?

In a NSAttributed type statement, I want to keep the existing attributed value and give it a new attributed value.
The problem is that replacingOccurrences is only possible for string types, as I want to give a new value every time the word appears in the entire sentence.
If I change NSAttributedString to string type, the attributed value is deleted. I must keep the existing values.
How can I do that?
To get this working,
1. First you need to find the indices of all the duplicate substrings existing in a string. To get that you can use this: https://stackoverflow.com/a/40413665/5716829
extension String {
func indicesOf(string: String) -> [Int] {
var indices = [Int]()
var searchStartIndex = self.startIndex
while searchStartIndex < self.endIndex,
let range = self.range(of: string, range: searchStartIndex..<self.endIndex),
!range.isEmpty
{
let index = distance(from: self.startIndex, to: range.lowerBound)
indices.append(index)
searchStartIndex = range.upperBound
}
return indices
}
}
2. Next you need to apply your desired attribute to substring at each index, i.e.
let str = "The problem is that replacingOccurrences Hello is only possible for string types, as I want to give Hello a new value every time Hello the word appears in the entire sentence Hello."
let indices = str.indicesOf(string: "Hello")
let attrStr = NSMutableAttributedString(string: str, attributes: [.foregroundColor : UIColor.blue])
for index in indices
{
//You can write your own logic to specify the color for each duplicate. I have used some hardcode indices
var color: UIColor
switch index
{
case 41:
color = .orange
case 100:
color = .magenta
case 129:
color = .green
default:
color = .red
}
attrStr.addAttribute(.foregroundColor, value: color, range: NSRange(location: index, length: "Hello".count))
}
Screenshot:
Let me know if you still face any issues. Happy Coding..🙂
You can use replaceCharacters. You can find the range of the substring you want to remove and use it as your range.