Swift - Parse a String to extract and edit numbers in it [closed] - swift

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
Given a String taken from the internet, such as:
"A dusting of snow giving way to moderate rain (total 10mm) heaviest on Thu night. Freeze-thaw conditions (max 8°C on Fri morning, min -2°C on Wed night). Mainly strong winds."
Using Swift 3, I want to convert the temperatures to Fahrenheit. So I need to find any numbers that have °C after them (including negative numbers); convert them to Fahrenheit, and then replace the integer back into the string.
I was originally trying to use: components(separatedBy: String). I did get it to work with this method. Although I think there is probably a better way.
func convertStringToFahrenheit (_ message: String) -> String{
var stringBuilder = String()
let stringArray = message.components(separatedBy: "°C")
for subString in stringArray {
if subString != stringArray.last {
if subString.contains("(max "){
let subStringArray = subString.components(separatedBy: "(max ")
stringBuilder.append(subStringArray[0])
stringBuilder.append("(max ")
if var tempInt = Int(subStringArray[1]){
tempInt = convertCelsiusToFahrenheit(tempInt)
stringBuilder.append(String(tempInt))
stringBuilder.append("°F")
}
}
else if subString.contains(", min "){
let subStringArray = subString.components(separatedBy: ", min ")
stringBuilder.append(subStringArray[0])
stringBuilder.append(", min ")
if var tempInt = Int(subStringArray[1]){
tempInt = convertCelsiusToFahrenheit(tempInt)
stringBuilder.append(String(tempInt))
stringBuilder.append("°F")
}
}
}
else {
stringBuilder.append(subString)
}
}
return stringBuilder
}

A job for regular expression.
The pattern "(-?\\d+)°C" searches for
an optional minus sign -?
followed by one or more digits \\d+
followed by °C
The group – within the parentheses – captures the degrees value.
var string = "A dusting of snow giving way to moderate rain (total 10mm) heaviest on Thu night. Freeze-thaw conditions (max 8°C on Fri morning, min -2°C on Wed night). Mainly strong winds."
let pattern = "(-?\\d+)°C"
do {
let regex = try NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: string, range: NSRange(location: 0, length: string.utf16.count))
for match in matches.reversed() { // reversed() is crucial to preserve the indexes.
let celsius = (string as NSString).substring(with: match.rangeAt(1))
let fahrenheit = Double(celsius)! * 1.8 + 32
let range = match.range
let start = string.index(string.startIndex, offsetBy: range.location)
let end = string.index(start, offsetBy: range.length)
string = string.replacingCharacters(in: start..<end, with: String(format: "%.0f°F", fahrenheit))
}
} catch {
print("Regex Error:", error)
}
print(string)
The most complicated part of the code is the conversion NSRange -> Range<String.Index>

Related

Remove characters from string [Swift] [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 1 year ago.
Improve this question
there is a line from the console that needs to be displayed in the application. Can anyone suggest how to clear the line?
"\u{1B}[2K\u{1B}[1G\u{1B}[32msuccess\u{1B}[39m Checking for changed pages - 0.000s"
And get in the end:
"success Checking for changed pages - 0.000s"
Use the regular expression mentioned in this answer
let str = "\u{1B}[2K\u{1B}[1G\u{1B}[32msuccess\u{1B}[39m Checking for changed pages - 0.000s"
let regex = try! NSRegularExpression(pattern: "\u{1B}(?:[#-Z\\-_]|\\[[0-?]*[ -/]*[#-~])")
let range = NSRange(str.startIndex..<str.endIndex, in: str)
let cleaned = regex.stringByReplacingMatches(in: str, options: [], range: range, withTemplate: "")
print(cleaned) // "success Checking for changed pages - 0.000s"
#Gereon's answer is one way to skin the cat. Here's another:
let s = "\u{1B}[2K\u{1B}[1G\u{1B}[32msuccess\u{1B}[39m Checking for changed pages - 0.000s"
guard let r = s.range(of: "Checking for changed pages") else {
fatalError("Insert code for what to do if the substring isn't found")
}
let cleaned = "success " + String(s[r.lowerBound...])
Here I just literally insert the "success". But if you really need to verify that it's in the string, that can be done too.
guard let r = s.range(of: "Checking for changed pages"), s.contains("success")
else
{
fatalError("Insert code for what to do if the substring isn't found")
}
To solve the more general problem of removing ANSI escape sequences, you'll need to parse them. Neither my simple solution, nor the regex solution will do it. You'll need to explicitly look for the possible valid codes that follow the escapes.
let escapeSequences: [String] =
[/* list of escape sequnces */
"[2K", "[1G", "[32m", "[39m", // etc...
]
let escapeChar = Character("\u{1B}")
var result = ""
var i = s.startIndex
outer: while i != s.endIndex
{
if s[i] == escapeChar
{
i = s.index(after: i)
for sequence in escapeSequences {
if s[i...].hasPrefix(sequence) {
i = s.index(i, offsetBy: sequence.distance(from: sequence.startIndex, to: sequence.endIndex))
continue outer
}
}
}
else { result.append(s[i]) }
i = s.index(after: i)
}
print(result)
The thing is, I think ANSI escape sequences can be combined in interesting ways so that what would be multiple escapes can be merged into a single one in some cases. So it can be more complex than just the simple parser I presented.

Swift String Tokenizer / Parser [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
Hello there fellow Swift devs!
I am a junior dev, and I'm trying to figure out a best way to tokenize / parse Swift String as an exercise.
What I have is a string which looks like this:
let string = "This is a {B}string{/B} and this is a substring."
What I would like to do is, tokenize the string, and change the "strings / tokens" inside the tags you see.
I can see using NSRegularExpression and it's matches, but it feels too generic. I would like to have only say 2 of these tags, that change the text. What would be the best approach in Swift 5.2^?
if let regex = try? NSRegularExpression(pattern: "\\{[a-z0-9]+\}", options: .caseInsensitive) {
let string = self as NSString
return regex.matches(in: self, options: [], range: NSRange(location: 0, length: string.length)).map {
// now $0 is the result? but it won't work for enclosing the tags :/
}
}
If the option of using html tags instead of {B}{/B} is acceptable, then you can use the StringEx library that I wrote for this purpose.
You can select a substring inside the html tag and replace it with another string like this:
let string = "This is a <b>string</b> and this is a substring."
let ex = string.ex
ex[.tag("b")].replace(with: "some value")
print(ex.rawString) // This is a <b>some value</b> and this is a substring.
print(ex.string) // This is a some value and this is a substring.
if necessary, you can also style the selected substrings and get NSAttributedString:
ex[.tag("b")].style([
.font(.boldSystemFont(ofSize: 16)),
.color(.black)
])
myLabel.attributedText = ex.attributedString
Not sure if you have solved it with NLTokenizer or not, but you can certainly solve it with Regx here is how (I have implemented it as generic, in future if you have to handle different kinds of tags and substite different string for them small tweak to the logic should do the job )
override func viewDidLoad() {
super.viewDidLoad()
let regexStr = "(\\{B\\}(\\s*\\w+\\s*)*\\{\\/B\\})"
let regex = try! NSRegularExpression(pattern: regexStr)
var string = "Sandeep {B}Bhandaari{/B} is here{B}Sandeep{/B}"
var foundRanges = [NSRange]()
regex.enumerateMatches(in: string, options: [], range: NSMakeRange(0, string.count)) { (match, flag, stop) in
if let matchRange = match?.range(at: 1) {
foundRanges.append(matchRange)
}
}
let substituteString = "abcd"
var replacedString = string as NSString
let foundRangesCount = foundRanges.count
var currentRange = 0
while foundRangesCount > currentRange {
let range = foundRanges[currentRange]
replacedString = replacedString.replacingCharacters(in: range, with: substituteString) as NSString
reEvaluateAllRanges(ranges: &foundRanges, byOffset: range.length - substituteString.count)
currentRange += 1
}
debugPrint(replacedString)
}
func reEvaluateAllRanges(ranges: inout [NSRange], byOffset: Int) {
var newFoundRange = [NSRange]()
for range in ranges {
newFoundRange.append(NSMakeRange(range.location - byOffset, range.length))
}
ranges = newFoundRange
}
Input: "Sandeep {B}Bhandaari{/B} is here"
Output: Sandeep abcd is here
Input: "Sandeep {B}Bhandaari{/B} is here{B}Sandeep{/B}"
Output: Sandeep abcd is hereabcd
Look at the edge case handling Longer strings replaced by smaller substitute strings and vice versa also detection of string enclosed in tag with / without space
EDIT 1:
Regx (\\{B\\}(\\s*\\w+\\s*)*\\{\\/B\\}) should be self explanatory, incase you need help with understanding it use cheat sheet
regex.enumerateMatches(in: string, options: [], range: NSMakeRange(0, string.count)) { (match, flag, stop) in
if let matchRange = match?.range(at: 1) {
foundRanges.append(matchRange)
}
}
I could have modified substring here itself, but if you have more than one match and if you mutate string evaluated ranges will be corrupted hence am saving all found ranges into an array and apply replace on each one of them later
let substituteString = "abcd"
var replacedString = string as NSString
let foundRangesCount = foundRanges.count
var currentRange = 0
while foundRangesCount > currentRange {
let range = foundRanges[currentRange]
replacedString = replacedString.replacingCharacters(in: range, with: substituteString) as NSString
reEvaluateAllRanges(ranges: &foundRanges, byOffset: range.length - substituteString.count)
currentRange += 1
}
Here am iterating through all found match ranges and replace character in range with substitute string, you can always have a switch / if else ladder inside while loop to look for different types of tags and pass different substitute strings for each tags
func reEvaluateAllRanges(ranges: inout [NSRange], byOffset: Int) {
var newFoundRange = [NSRange]()
for range in ranges {
newFoundRange.append(NSMakeRange(range.location - byOffset, range.length))
}
ranges = newFoundRange
}
This function modifies all the ranges in array using the offset, remember you need to only modify range's location, length remains same
One bit of optimisation you can do is probably get rid of ranges from array for which you have already applied substitute strings

Converting a CharacterSet to a String or to an array or Set of characters [duplicate]

This question already has answers here:
What characters are in whitespaceAndNewlineCharacterSet()?
(2 answers)
NSArray from NSCharacterSet
(9 answers)
Closed 3 years ago.
Fondation has CharacterSet struct (briged to NSCharacterSet) for managing sets of characters, for example when working with Formatter instances. Surprisingly, CharacterSet is not a Set, although the functionality and purpose is totally the same. Unfortunately CharacterSet is not a Collection ether, so right now I have no idea how to retrieve its elements.
// We can initialize with String
let wrongCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
// but how can we get the characters back ?
var chSet = CharacterSet.decimalDigits
let chString = String(chSet) // doesn't work
let chS = Set(chSet) // doesn't work
let chArr = Array(chSet) // doesn't work
I've slightly modified the solution, found in answers pointed out by #Larme and #vadian. Both answers end up with the same algorithm. All I wanted was to look at the contents of the set. Yes, it is not a common thing to want to.
It turns out that the only way to get all the elements of CharacterSet is to loop through all possible unicode scalars and check if they belong to the set. Feels so strange to me in the word where we can switch between Sets, Arrays and even Dictionaries so easy.
The reason for modification is and attempt to speed up the function. My rough experiments show that using scalars is 30% faster even if we create a string in the end.
extension CharacterSet {
func allUnicodeScalars() -> [UnicodeScalar] {
var result: [UnicodeScalar] = []
for plane in Unicode.UTF8.CodeUnit.min...16 where self.hasMember(inPlane: plane) {
for unicode in Unicode.UTF32.CodeUnit(plane) << 16 ..< Unicode.UTF32.CodeUnit(plane + 1) << 16 {
if let uniChar = UnicodeScalar(unicode), self.contains(uniChar) {
result.append(uniChar)
}
}
}
return result
}
}
// Testing and timing
printTimeElapsedWhenRunningCode(title:"allUnicodeScalars()") {
print(String.UnicodeScalarView(chSet.allUnicodeScalars()))
}
// Time elapsed for allUnicodeScalars(): 1.936843991279602 s.
printTimeElapsedWhenRunningCode(title:"allCharacters()") {
print(String(chSet.allCharacters()))
}
// Time elapsed for allCharacters(): 2.9846099615097046 s.
//Timing functions (for reference):
private func printTimeElapsedWhenRunningCode(title:String, operation:()->()) {
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
print("Time elapsed for \(title): \(timeElapsed) s.")
}
private func timeElapsedInSecondsWhenRunningCode(operation: ()->()) -> Double {
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
return Double(timeElapsed)
}
UPD: Yes, the question is a duplicate, and a better answer exists.

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 can I count the number of sentences in a given text in Swift? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I wanted to create a playground that would count the number of sentences of a given text.
let input = "That would be the text . it hast 3. periods. "
func sentencecount() {
let periods = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters)
let periods = input.components(separatedBy: spaces)
let periods2 = Int (words.count)
print ("The Average Sentence length is \(periods2)")
}
sentencecount()
You can use enumerateSubstrings(in: Range) and use option .bySentences:
let input = "Hello World !!! That would be the text. It hast 3 periods."
var sentences: [String] = []
input.enumerateSubstrings(in: input.startIndex..., options: .bySentences) { (string, range, enclosingRamge, stop) in
sentences.append(string!)
}
An alternative is to use an array of Substrings instead of Strings:
var sentences: [Substring] = []
input.enumerateSubstrings(in: input.startIndex..., options: .bySentences) { (string, range, enclosingRamge, stop) in
sentences.append(input[range])
}
print(sentences) // "["Hello World !!! ", "That would be the text. ", "It hast 3 periods."]\n"
print(sentences.count) // "3\n"
This should work :
let input = "That would be the text . it hast 3. periods. "
let occurrencies = input.characters.filter { $0 == "." || $0 == "?" }.count
print(occurrencies)
//result 3
Just add the character in charset by which you are going to differentiate your sentences:
I am assuming ? . , for now:
let input = "That would be the text. it hast 3? periods."
let charset = CharacterSet(charactersIn: ".?,")
let arr = input.components(separatedBy: charset)
let count = arr.count - 1
Here arr would be:
["That would be the text", " it hast 3", " periods", ""]
decrease count by 1, to get actual sentences.
Note: If you don't want to consider " , " then remove it from charset.
As far as i can see that you need to split them using . and trimming whitespaces as the following:
func sentencecount () {
let result = input.trimmingCharacters(in: .whitespaces).split(separator: ".")
print ("The Average Sentence length is \(result.count)") // 3
}
Good luck!