I am looking for a way to detect years e.g. 2019. The requirements I think would be that the numbers are in a row, have four digits and are not adjacent to letters or special characters. So I'd like to get the method to return "2019" in each of the following cases:
"2019"
"in 2019
"Before 2019, all IOS apps were written in one of 2 ways"
But exclude it from:
"1234z20191234
There are a lot of ways to detect the numbers in a string as seen here such as
let newString = origString
.components(separatedBy:CharacterSet.decimalDigits.inverted)
.joined(separator: "")
But they don't pull out each series of numbers to test for length and adjacency.
Data detector can try to pull out a date but going from the possible date to the year that might have been in the starting text seems error prone e.g. working with something like:
“2018-08-31 04:00:00 +0000”, “America/Los_Angeles”, 18000.0
Can anyone suggest a reliable way to retrieve a numerical year from a string?
You might use regular expression, searching for four digits ([0-9]{4}) between word boundaries (\b), e.g.
let strings = [
"2019",
"in 2019",
"Before 2019, all IOS apps were written in one of 2 ways",
"1234z20191234"
]
for string in strings {
if let range = string.range(of: #"\b[0-9]{4}\b"#, options: .regularExpression) {
let year = String(string[range])
print(year)
} else {
print("No year found")
}
}
Producing:
2019
2019
2019
No year found
Note, the above uses Swift 5’s Extended String Delimiters, #" and "#. If doing this in earlier Swift versions, you’ll want to just escape the backslashes with yet another backslash:
if let range = string.range(of: "\\b[0-9]{4}\\b", options: .regularExpression) {
...
}
Clearly if you want to narrow the range of four digit numbers recognized as years, you’d have to tweak the regular expression or supplement it with some logic. But your question doesn’t identify what criteria you want to use to detect years.
You can do this with a regular expression. This code will find all years in a given string. You can set the check to confirm the number is within whatever range you wish to accept.
func findYears(in text: String) -> [String] {
let yearRE = try! NSRegularExpression(pattern: "(?:\\b)[0-9]{4}(?:\\b)")
var results = [String]()
yearRE.enumerateMatches(in: text, range: NSRange(text.startIndex..<text.endIndex, in: text)) { (result, flags, stop) in
if let result = result {
let match = String(text[Range(result.range(at: 0), in: text)!])
// Set whatever range you wish to accept
if let year = Int(match), year > 1600 && year < 2200 {
results.append(match)
}
}
}
return results
}
let yearStrings = [
"2019",
"in 2019 and 2020",
"Before 2019, all IOS apps were written in one of 2 ways",
"1234z20191234",
"2018-08-31 04:00:00 +0000",
]
for yearString in yearStrings {
print(findYears(in: yearString))
}
Output:
["2019"]
["2019", "2020"]
["2019"]
[]
["2018"]
Related
I am having a string in that I have getting a tag </p> and I want to append </b> ahead of </p>
let str = "<p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"
I want this string as:
let newStr = #"<b><p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p></b><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"#
If you want to replace all occurrences of a string with another string you can use replacingOccurrences(of:with:). If you want to replace the first occurrence find range of the substring and replace with new substring
if let range = str.range(of: "<p>") {
str.replaceSubrange(range, with: "<b><p>")
}
if let range = str.range(of: "</p>") {
str.replaceSubrange(range, with: "</b></p>")
}
Updated due comment (just first tag) -> See history if you want simpler edition
extension String {
enum TagRange {
case first
case last
case any
}
func addingHTMLTag(_ wrapperTag: String, around tagToWrap: String, witchIs tagRange: TagRange = .any) -> String {
let range: NSRange?
let regxPattern = "<\(tagToWrap)>(.+?)</\(tagToWrap)>"
let regx = try! NSRegularExpression(pattern: regxPattern, options: [])
let allRange = NSRange(location: 0, length: self.utf8.count)
switch tagRange {
case .first: range = regx.rangeOfFirstMatch(in: self, options: [], range: allRange)
case .last: range = regx.matches(in: self, options: [], range: allRange).last?.range
case .any: range = nil
}
if let range = range {
let openTagged = (self as NSString).replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>", range: range)
let offsetRange = NSRange(location: range.location + (wrapperTag.count+2) , length: range.length)
return (openTagged as NSString).replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>", range: offsetRange)
} else {
let openTagged = replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>")
return openTagged.replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>")
}
}
}
usage:
let newStr = str.addingHTMLTag("b", around: "p", witchIs: .first)
This method also checks if the tag you want to wrap around is valid. Maybe there is no closing tag or no opening tag or invalid opening and closing tags order. or maybe there is a<p> in the text itself! that you don't want to wrap.
I want to use iOS Speech API to recognize mathematical expressions. It works okay for things like two plus four times three - reads it as 2+4*3, but when I start expression with 1 it always reads it as "One". When "One" is in the middle of expression it works as expected.
I figured out that when I set SFSpeechAudioBufferRecognitionRequest property taskHint to .search when displaying live results it recognizes 1 as "1" properly at first but at the end changes it to "One"
Is there a way to configure it to recognize only numbers?
Or just force to read "One" as "1" always?
Or the only way to fix it is to format result string on my own?
I have the same problem, but looks like there is no way to configure it.
I write this extension for my code, I'm checking every segment with this.
extension String {
var numericValue: NSNumber? {
//init number formater
let numberFormater = NumberFormatter()
//check if string is numeric
numberFormater.numberStyle = .decimal
guard let number = numberFormater.number(from: self.lowercased()) else {
//check if string is spelled number
numberFormater.numberStyle = .spellOut
//change language to spanish
//numberFormater.locale = Locale(identifier: "es")
return numberFormater.number(from: self.lowercased())
}
// return converted numeric value
return number
}
}
For example
{
let numString = "1.5"
let number = numString.numericValue //1.5
// or
let numString = "Seven"
let number = numString.numericValue //7
}
My Swift app involves searching through text in a UITextView. The user can search for a certain substring within that text view, then jump to any instance of that string in the text view (say, the third instance). I need to find out the integer value of which character they are on.
For example:
Example 1: The user searches for "hello" and the text view reads "hey hi hello, hey hi hello", then the user presses down arrow to view second instance. I need to know the integer value of the first h in the second hello (i.e. which # character that h in hello is within the text view). The integer value should be 22.
Example 2: The user searches for "abc" while the text view reads "abcd" and they are looking for the first instance of abc, so the integer value should be 1 (which is the integer value of that a since it's the first character of the instance they're searching for).
How can I get the index of the character the user is searching for?
Xcode 11 • Swift 5 or later
let sentence = "hey hi hello, hey hi hello"
let query = "hello"
var searchRange = sentence.startIndex..<sentence.endIndex
var indices: [String.Index] = []
while let range = sentence.range(of: query, options: .caseInsensitive, range: searchRange) {
searchRange = range.upperBound..<searchRange.upperBound
indices.append(range.lowerBound)
}
print(indices) // "[7, 21]\n"
Another approach is NSRegularExpression which is designed to easily iterate through matches in an string. And if you use the .ignoreMetacharacters option, it will not apply any sophisticated wildcard/regex logic, but will just look for the string in question. So consider:
let string = "hey hi hello, hey hi hello" // string to search within
let searchString = "hello" // string to search for
let matchToFind = 2 // grab the second occurrence
let regex = try! NSRegularExpression(pattern: searchString, options: [.caseInsensitive, .ignoreMetacharacters])
You could use enumerateMatches:
var count = 0
let range = NSRange(string.startIndex ..< string.endIndex, in: string)
regex.enumerateMatches(in: string, range: range) { result, _, stop in
count += 1
if count == matchToFind {
print(result!.range.location)
stop.pointee = true
}
}
Or you can just find all of them with matches(in:range:) and then grab the n'th one:
let matches = regex.matches(in: string, range: range)
if matches.count >= matchToFind {
print(matches[matchToFind - 1].range.location)
}
Obviously, if you were so inclined, you could omit the .ignoreMetacharacters option and allow the user to perform regex searches, too (e.g. wildcards, whole word searches, start of word, etc.).
For Swift 2, see previous revision of this answer.
I have a UILabel to show input text (which has decimal numbers). What I am trying to do is, that the UILabel text can be shown as per locale. E.g.-
"1234.56" should be shown as "1,234.56" for US locale, "1.234,56" for DE (Germany) locale and so on.
Below is what I have done to achieve this -
NSNumberFormatter.numberStyle = NSNumberFormatterStyle.DecimalStyle
NSNumberFormatter.locale = NSLocale(localeIdentifier: language)
NSNumberFormatter.maximumFractionDigits = 16
let displayVal: Double = NSNumberFormatter.numberFromString(displayText.text!).doubleValue
displayText.text = NSNumberFormatter.stringFromNumber(displayVal)
Where displayText is the UILabel to display input.
It works fine until 4 digits i.e. "1234" was shown as "1,234" (for US locale). But as soon as 5th digit gets added, nil returned for displayVal constant variable.
I could guess from the function definition that there is failure in formatting displayText text string which may be "1,2345" when numberFromString tried to format it as per US locale but failed as this does not comply to US locale format.
Can anyone suggest a way to fix this or nicer way of implementation?
You can do something like the following, namely strip out any thousands separators, get a number from that, and then reformat with the thousands separator:
#IBAction func didEditingChanged(_ sender: UITextField) {
guard let text = sender.text else { return }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// get thousands separator
let thousandsSeparator = formatter.groupingSeparator ?? ","
// strip thousands separators out
let stripped = String(text.filter { return String($0) != thousandsSeparator })
// now, re-insert them, if any
if let number = formatter.numberFromString(stripped) {
sender.text = formatter.stringFromNumber(number)
}
}
Assuming you're hooking this up to "Editing Changed" so that it's constantly updated, this introduces a few issues. For example, to enter the trailing decimal place, you might have to manually check for that and allow that to be preserved:
#IBAction func didEditingChanged(_ sender: UITextField) {
guard let text = sender.text else { return }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// get thousands separator
let thousandsSeparator = formatter.groupingSeparator ?? ","
// strip thousands separators out
let stripped = String(text.filter { return String($0) != thousandsSeparator })
// see if decimal place found, and if so, set fractional digits accordingly
let decimalSeparator = formatter.decimalSeparator ?? "."
var fractionalDigits: Int?
if let decimalSeparatorIndex = text.range(of: decimalSeparator)?.lowerBound {
fractionalDigits = text[decimalSeparatorIndex...].count(of: "0123456789")
formatter.minimumFractionDigits = fractionalDigits!
}
guard
let number = formatter.number(from: stripped),
var result = formatter.string(from: number)
else {
// do whatever is appropriate if string isn't a number at all
return
}
// re-add trailing decimal place only if decimal separator was found, but no fractional digits were
if fractionalDigits == 0 {
result += decimalSeparator
}
// update text field
sender.text = result
}
Note, that uses this little extension:
extension StringProtocol {
func count(of string: String) -> Int {
return reduce(0) { $0 + (string.contains($1) ? 1 : 0) }
}
}
There are many other refinements you could tackle. For example, if the user is not at the end of the text field when they edit, this will reset the selectedTextRange. You also should probably implement shouldChangeCharactersIn to make sure that you cannot enter value at all where stripped would fail. Etc.
But hopefully this illustrates one approach for capturing text input and updating the result with the formatted decimal number.
For Swift 2 rendition, see previous revision of this answer.
You need to make sure the number formatter matches the strings that you pass to it. If there is any mismatch, it is going to return nil, and the doubleValue call is going to return 0.
If you have strings formatted for the US, with a grouping separator of a comma, then you should set the groupingSeparator property of the number formatter to a comma. Then the formatter should handle input strings like "1,234". As you say, "1,2345" is not a normal US format for displaying a number.
You might need to change the groupingSize setting as well (The string "1,2345" would have a groupingSize of 4, if I'm not mistaken.
Where are the strings you are parsing coming from? Do you know what locale they use? Is the user entering "1,2345" on a device that is set up using the en-US locale"? If so, why? That's not normal formatting for US English.
So here is the string s:
"Hi! How are you? I'm fine. It is 6 p.m. Thank you! That's it."
I want them to be separated to a array as:
["Hi", "How are you", "I'm fine", "It is 6 p.m", "Thank you", "That's it"]
Which means the separators should be ". " + "? " + "! "
I've tried:
let charSet = NSCharacterSet(charactersInString: ".?!")
let array = s.componentsSeparatedByCharactersInSet(charSet)
But it will separate p.m. to two elements too. Result:
["Hi", " How are you", " I'm fine", " It is 6 p", "m", " Thank you", " That's it"]
I've also tried
let array = s.componentsSeparatedByString(". ")
It works well for separating ". " but if I also want to separate "? ", "! ", it become messy.
So any way I can do it? Thanks!
There is a method provided that lets you enumerate a string. You can do so by words or sentences or other options. No need for regular expressions.
let s = "Hi! How are you? I'm fine. It is 6 p.m. Thank you! That's it."
var sentences = [String]()
s.enumerateSubstringsInRange(s.startIndex..<s.endIndex, options: .BySentences) {
substring, substringRange, enclosingRange, stop in
sentences.append(substring!)
}
print(sentences)
The result is:
["Hi! ", "How are you? ", "I\'m fine. ", "It is 6 p.m. ", "Thank you! ", "That\'s it."]
rmaddy's answer is correct (+1). A Swift 3 implementation is:
var sentences = [String]()
string.enumerateSubstrings(in: string.startIndex ..< string.endIndex, options: .bySentences) { substring, substringRange, enclosingRange, stop in
sentences.append(substring!)
}
You can also use regular expression, NSRegularExpression, though it's much hairier than rmaddy's .bySentences solution. In Swift 3:
var sentences = [String]()
let regex = try! NSRegularExpression(pattern: "(^|\\s+)(\\w.*?[.!?]+)(?=(\\s+|$))")
regex.enumerateMatches(in: string, range: NSMakeRange(0, string.characters.count)) { match, flags, stop in
sentences.append((string as NSString).substring(with: match!.rangeAt(2)))
}
Or Swift 2:
let regex = try! NSRegularExpression(pattern: "(^|\\s+)(\\w.*?[.!?]+)(?=(\\s+|$))", options: [])
var sentences = [String]()
regex.enumerateMatchesInString(string, options: [], range: NSMakeRange(0, string.characters.count)) { match, flags, stop in
sentences.append((string as NSString).substringWithRange(match!.rangeAtIndex(2)))
}
The [.!?] syntax matches any of those three characters. The | means "or". The ^ matches the start of the string. The $ matches the end of the string. The \\s matches a whitespace character. The \\w matches a "word" character. The * matches zero or more of the preceding character. The + matches one or more of the preceding character. The (?=) is a look-ahead assertion (e.g. see if there's something there, but don't advance through that match).
I've tried to simplify this a bit, and it's still pretty complicated. Regular expressions offer rich text pattern matching, but, admittedly, it is a little dense when you first use it. But this rendition matches (a) repeated punctuation (e.g. "Thank you!!!"), (b) leading spaces, and (c) trailing spaces, too.
If the splitting basis is something a little more esoteric than sentences, this extension could work.
extension String {
public func components(separatedBy separators: [String]) -> [String] {
var output: [String] = [self]
for separator in separators {
output = output.flatMap { $0.components(separatedBy: separator) }
}
return output.map { $0.trimmingCharacters(in: .whitespaces)}
}
}
let artists = "Rihanna, featuring Calvin Harris".components(separated by: [", with", ", featuring"])
I tried to find a regex to solve this too: (([^.!?]+\s)*\S+(\.|!|\?))
Here the explanation from regexper and an example
Well I've found a regex too from here
var pattern = "(?<=[.?!;…])\\s+(?=[\\p{Lu}\\p{N}])"
let s = "Hi! How are you? I'm fine. It is 6 p.m. Thank you! That's it."
let sReplaced = s.stringByReplacingOccurrencesOfString(pattern, withString:"[*-SENTENCE-*]" as String, options:NSStringCompareOptions.RegularExpressionSearch, range:nil)
let array = sReplaced.componentsSeparatedByString("[*-SENTENCE-*]")
Perhaps it's not a good way as it has to first replace and than separate the string. :)
UPDATE:
For regex part, if you also want to match Chinese/Japanese punctuations (which space after each punctuation is not necessary), you can use the following one:
((?<=[.?!;…])\\s+|(?<=[。!?;…])\\s*)(?=[\\p{L}\\p{N}])