Swift range: not including first element - swift

For getting the elements in a certain range up to but excluding index, the following command is used:
let substr=mystr[..<index]
But how can I get a certain range, beginning from the first element after index? It should be like:
let substr=mystr[index<..] //does not work

You can use index(after:) method to get the following index:
let mystr = "Hello"
let index = mystr.startIndex
if index < mystr.endIndex {
let substr = mystr[mystr.index(after: index)...]
print(substr) // "ello\n"
}

Related

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

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)

how to run multiples NSRegularExpression once

I have a bunch of NSRegularExpression and I want to run it once. Anyone knows how to do it ?
For the moment I do it in a .forEach, for performance reasons I do not think this is the best idea
Each NSRegularExpression needs to match a different pattern, after the matching I need to deal with each different kind of match. As example if I match with the first regex in my array I need to make something different from the second etc...
let test: String = "Stuff"
let range: NSRange = // a range
var regexes = [NSRegularExpression] = // all of my regexes
regexes.forEach { $0.matches(in: text, options: [], range: range) }
Thanks for you help
You may be able to evaluate several regular expressions as one if you concatenate them using capture groups and an OR expressions.
If you want to search for: language, Objective-C and Swift strings you should use a pattern like this: (language)|(Objective-C)|(Swift). Each capture group has an order number, so if language is found in the source string the match object provides the index number.
You can used the code in this playground sample:
import Foundation
let sourceString: String = "Swift is a great language to program, but don't forget Objective-C."
let expresions = [ "language", // Expression 0
"Objective-C", // Expression 1
"Swift" // Expression 2
]
let pattern = expresions
.map { "(\($0))" }
.joined(separator: "|") // pattern is defined as : (language)|(Objective-C)|(Swift)
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let matches = regex?.matches(in: sourceString, options: [],
range: NSRange(location: 0, length: sourceString.utf16.count))
let results = matches?.map({ (match) -> (Int, String) in // Array of type (Int: String) which
// represents index of expression and
// string capture
let index = (1...match.numberOfRanges-1) // Go through all ranges to test which one was used
.map{ Range(match.range(at: $0), in: sourceString) != nil ? $0 : nil }
.compactMap { $0 }.first! // Previous map return array with nils and just one Int
// with the correct position, lets apply compactMap to
// get just this number
let foundString = String(sourceString[Range(match.range(at: 0), in: sourceString)!])
let position = match.range(at: 0).location
let niceReponse = "\(foundString) [position: \(position)]"
return (index - 1, niceReponse) // Let's substract 1 to index in order to match zero based array index
})
print("Matches: \(results?.count ?? 0)\n")
results?.forEach({ result in
print("Group \(result.0): \(result.1)")
})
If you run it the result is:
How many matches: 3
Expression 2: Swift [position: 0]
Expression 0: language [position: 17]
Expression 1: Objective-C [position: 55]
I hope I understood correctly your question and this code helps you.

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.

Update a Range in Swift 3

I'm trying to search a String for regex using the following snippet (it's in an extension for String):
var range = self.startIndex..<self.endIndex
while range.lowerBound < range.upperBound {
if let match = self.range(of: regex, options: .regularExpression, range: range, locale: nil) {
print(match)
range = ????? <============ this line, how do I update the range?
}
}
It will correctly find the first occurance, but then I don't know how to change the range to the position of the match to search the remainder of the string.
lowerBound and upperBound are immutable properties of the range,
so you have to create a new range, starting at match.upperBound.
Also the loop should terminate if no match is found.
That can be achieved by moving the binding
let match = ... into the where condition.
var range = self.startIndex..<self.endIndex
while range.lowerBound < range.upperBound,
let match = self.range(of: regex, options: .regularExpression, range: range) {
print(match) // the matching range
print(self.substring(with: match)) // the matched string
range = match.upperBound..<self.endIndex
}
This can still lead to an infinite loop if an empty string matches
the pattern (e.g. for regex = "^"). This can be solved, but
as an alternative, use NSRegularExpression to get a list of all
matches (see for example Swift extract regex matches).

Get a value at index from range

I want to retrieve a random emoji inside the range.
let emojiRanges = [
0x1F601...0x1F64F,
0x1F680...0x1F6C0,
]
let flattenEmoji = emojiRanges.flatten()
// the loop for emoji works
for i in flattenEmoji {
let st = String(format:"0x%2X %#", i, String(UnicodeScalar(i)))
print(st)
}
// but this is not possible to obtain value at wanted index
//there is a compiler error:
let randomSign = String(UnicodeScalar(flattenEmoji[arc4random_uniform(UInt32(flattenEmoji.count))]))
print("RANDOM \(randomSign)")
the error:
ViewController.swift:68:67: Cannot subscript a value of type
'FlattenBidirectionalCollection<[Range]>' (aka
'FlattenBidirectionalCollection>>') with an index of
type 'UInt32'
What is the proper way to get a result?
The problem is that flatten() is lazily applied, and therefore returns a special FlattenBidirectionalCollection, which is indexed by a FlattenBidirectionalCollectionIndex, rather than an Int.
The simplest solution therefore would be to simply use the Array(_:) constructor (or flatMap(_:)) in order to eagerly apply the flattening of the ranges, which will create an array that you can then subscript with an Int.
let flattenEmoji = Array(emojiRanges.flatten()) // In Swift 3, flatten() is named joined()
let randomIndex = Int(arc4random_uniform(UInt32(flattenEmoji.count)))
let randomSign = String(UnicodeScalar(flattenEmoji[randomIndex]))
If you wish to keep the flattening being lazily applied, you could subscript the FlattenBidirectionalCollection directly (for Swift 2) through using advancedBy(_:) on the collection's startIndex:
let randomIndex = flattenEmoji.startIndex.advancedBy(Int(arc4random_uniform(UInt32(flattenEmoji.count))))
let randomSign = String(UnicodeScalar(flattenEmoji[randomIndex]))
In Swift 3, as collections move their indices, you'd want use the collection's index(_:offsetBy:) method instead:
let randomIndex = flattenEmoji.index(flattenEmoji.startIndex, offsetBy: Int(arc4random_uniform(UInt32(flattenEmoji.count))))
Change emojiRanges declaration to this:
let emojiRanges = Array(0x1F601...0x1F64F) + Array(0x1F680...0x1F6C0)
then life will become much easier.
for i in emojiRanges {
let st = String(format:"0x%2X %#", i, String(UnicodeScalar(i)))
print(st)
}
in randomSign you should convert index to Int
let randomSign = String(UnicodeScalar(emojiRanges[Int(arc4random_uniform(UInt32(emojiRanges.count)))]))
print("RANDOM \(randomSign)")