Swift sort NSArray - ios

I have a problem with my sorting algorithm.
My NSArray (here vcd.signals.key) contains values of Strings for example:
"x [0]", "x [18]", "x [15]", "x [1]"...
When I try to sort this the result ends up in
"x [0]", "x [15]", "x [18]", "x [1]"
instead of:
"x [0]", "x [1]", "x [15]", "x [18]"
This is my code:
let sortedKeys = sorted(vcd.signals.keys) {
var val1 = $0 as! String
var val2 = $1 as! String
return val1 < val2
}
Any idea how I can fix this issue?

Your problem come associated with your comparison , for example see what happen when you compare the two following strings:
println("x [15]" < "x [1]") // true
This is because the default lexicography comparer goes character for character, position by position comparing ,and of course 5 in position 3 is less than ] in position 3:
println("5" < "]") // true
For the explained above you need to create you own comparer but , only compare for the numbers inside the [$0]. For achieve this I use regular expressions to match any numbers inside the brackets like in the following way:
func matchesForRegexInText(regex: String!, text: String!) -> [String] {
let regex = NSRegularExpression(pattern: regex,
options: nil, error: nil)!
let nsString = text as NSString
let results = regex.matchesInString(text,
options: nil, range: NSMakeRange(0, nsString.length))
as! [NSTextCheckingResult]
return map(results) { nsString.substringWithRange($0.range)}
}
var keysSorted = keys.sorted() {
var key1 = $0
var key2 = $1
var pattern = "([0-9]+)"
var m1 = self.matchesForRegexInText(pattern, text: key1)
var m2 = self.matchesForRegexInText(pattern, text: key2)
return m1[0] < m2[0]
}
In the above regular expression I assume that the numbers only appears inside the brackets and match any number inside the String, but feel free to change the regular expression if you want to achieve anything more. Then you achieve the following:
println(keysSorted) // [x [0], x [1], x [15], x [18]]
I hope this help you.

The issue you are running into is the closing brace character ']' comes after digits. This means that "18" is less than "1]". As long as all your strings share the form of "[digits]" then you can remove the closing brace, sort the strings, add the closing brace back to your final array. The code below works for Swift 2:
let arr = ["x [0]", "x [18]", "x [15]", "x [1]"]
let sorted = arr.map { $0.substringToIndex($0.endIndex.predecessor()) }.sort().map { $0 + "]" }
print(sorted)

Related

How to put and sort word in NSCountedSet in swift?

I'm try to getting most duplicated word from string with this code.
let text = """
aa bb aa bb aa bb cc dd dd cc zz zz cc dd zz
"""
let words = text.unicodeScalars.split(omittingEmptySubsequences: true, whereSeparator: { !CharacterSet.alphanumerics.contains($0) })
.map { String($0) }
let wordSet = NSCountedSet(array: words)
let sorted = wordSet.sorted { wordSet.count(for: $0) > wordSet.count(for: $1) }
print(sorted.prefix(3))
result is
[cc, dd, aa]
Currently, it put all words, even it is a single charcter.
What I'm going to do is,
put a word to NSCountedSet which has more than one character.
if words in NSCountedSet have same count, sort it alphabetically.
(desired result is aa ,cc, dd)
And if it is possible..
omit parts of speech from the string, such as 'and, a how,of,to,it,in on, who '....etc
Let's consider this string:
let text = """
She was young the way an actual young person is young.
"""
You could use a linguistic tagger :
import NaturalLanguage
let options = NSLinguisticTagger.Options.omitWhitespace.rawValue
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: Int(options))
To count the multiplicity of each word I'll be using a dictionary:
var dict = [String : Int]()
Let's define the accepted linguistic tags (you change these to your liking) :
let acceptedtags: Set = ["Verb", "Noun", "Adjective"]
Now let's parse the string, using the linguistic tagger :
let range = NSRange(location: 0, length: text.utf16.count)
tagger.string = text
tagger.enumerateTags(
in: range,
scheme: .nameTypeOrLexicalClass,
options: NSLinguisticTagger.Options(rawValue: options),
using: { tag, tokenRange, sentenceRange, stop in
guard let range = Range(tokenRange, in: text)
else { return }
let token = String(text[range]).lowercased()
if let tagValue = tag?.rawValue,
acceptedtags.contains(tagValue)
{
dict[token, default: 0] += 1
}
// print(String(describing: tag) + ": \(token)")
})
Now the dict has the desired words with their multiplicity
print("dict =", dict)
As you can see a Dictionary is an unoreded collection. Now let's introduce some law and order:
let ordered = dict.sorted {
($0.value, $1.key) > ($1.value, $0.key)
}
Now let's get the keys πŸ— only:
let mostFrequent = ordered.map { $0.key }
and print the three most frequent words :
print("top three =", mostFrequent.prefix(3))
To get the topmost frequent words, it would be more efficient to use a Heap (or a Trie) data structure, instead of having to hash every word, sort them all by frequency, and then prefixing. It should be a fun exercise πŸ˜‰.

Swift 4.2 extract substring using multiple characters are delimiter

I'm new to Swift and after going through the Apple documentation and other sources is not clear for me how can I extract a substring using more than one character as delimiter. For example: I have a string which looks like:
A.1 value1
B.2 value2
E value3
C value4
and need to assign the values 1 - 4 to different variables.
β€’ Possible solution:
1. Separate all the elements (separator: white space)
2. Iterate 2 by 2 and use a key/value system, like a Dictionary.
3. Read each values from the keys afterward
Step 1:
let string = "A.1 value1 B.2 value2 E value3 C value4"
let components = string.components(separatedBy: CharacterSet.whitespaces)
Step 2:
var dictionary: [String: String] = [:]
stride(from: 0, to: components.count - 1, by: 2).forEach({
dictionary[components[$0]] = components[$0+1]
})
or
let dictionary = stride(from: 0, to: components.count - 1, by: 2).reduce(into: [String: String]()) { (result, currentInt) in
result[components[currentInt]] = components[currentInt+1]
}
dictionary is ["A.1": "value1", "C": "value4", "E": "value3", "B.2": "value2"]
Inspiration for the stride(from:to:) that I rarely use.
Step 3:
let name = dictionary["A.1"]
let surname = dictionary["C"]
β€’ Potential issues:
If you have:
let string = "A.1 value One B.2 value2 E value3 C value4"
You want "value One", and since there is a space, you'll get some issue because if will give a false result (since there is the separator).
You'll get: ["A.1": "value", "One": "B.2", "value2": "E", "value3": "C"] for dictionary.
So you could use instead a regex: A.1(.*)B.2(.*)E(.*)C(.*) (for instance).
let string = "A.1 value One B.2 value2 E value3 C value4"
let regex = try! NSRegularExpression(pattern: "A.1(.*)B.2(.*)E(.*)C(.*)", options: [])
regex.enumerateMatches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) { (result, flags, stop) in
guard let result = result,
let aValueRange = Range(result.range(at: 1), in: string),
let bValueRange = Range(result.range(at: 2), in: string),
let cValueRange = Range(result.range(at: 4), in: string),
let eValueRange = Range(result.range(at: 3), in: string) else { return }
let aValue = string[aValueRange].trimmingCharacters(in: CharacterSet.whitespaces)
print("aValue: \(aValue)")
let bValue = string[bValueRange].trimmingCharacters(in: CharacterSet.whitespaces)
print("bValue: \(bValue)")
let cValue = string[cValueRange].trimmingCharacters(in: CharacterSet.whitespaces)
print("cValue: \(cValue)")
let eValue = string[eValueRange].trimmingCharacters(in: CharacterSet.whitespaces)
print("eValue: \(eValue)")
}
Output:
$>aValue: value One
$>bValue: value2
$>cValue: value4
$>eValue: value3
Note that the trim could be inside the regex, but I don't especially like having too complex regexes.
I like regular expressions for this sort of thing.
I'm going to take you very literally and assume that the substrings to be found are preceded by "A.1", "B.2", "E", and "C", and are all preceded and followed by a space except for the last substring which is followed by the end of the original string. Moreover I'm going to assume very simple-mindedly that the delimiters such as "E" cannot appear in our string in any other way. Then we can capture each substring with an appropriate pattern:
let s = "A.1 harpo B.2 chico E zeppo C groucho"
let p1 = "^A\\.1 (.*) B\\.2 "
let p2 = " B\\.2 (.*) E "
let p3 = " E (.*) C "
let p4 = " C (.*)$"
let patts = [p1,p2,p3,p4]
var result = [String]()
for patt in patts {
let regex = try! NSRegularExpression(pattern: patt, options: [])
if let match = regex.firstMatch(in: s, options: [],
range: NSRange(s.startIndex..<s.endIndex, in: s)) {
let r = match.range(at: 1)
result.append((s as NSString).substring(with: r))
}
}
// result is now ["harpo", "chico", "zeppo", "groucho"]
We now have the four desired substrings extracted into an array, and dealing with them from there is trivial.
Observe that we make no assumptions about spaces. The above works perfectly well even if the target substrings contain spaces, because we are appealing only to the delimiters. For example, if the original string is
let s = "A.1 the rain B.2 in spain E stays mainly C in the plain"
then result is the array
["the rain", "in spain", "stays mainly", "in the plain"]
I should point out, however, that another way to do this sort of thing is to walk the original string with a Scanner. You might prefer this because regular expressions are not really needed here, and if you don't know regular expressions you'll find this kind of walk much clearer. So here it is rewritten to use a scanner. Note that we end up with four Optional NSString objects, because Scanner is actually an Objective-C Cocoa Foundation thing, but it isn't difficult to turn those into String objects as needed:
let s = "A.1 the rain B.2 in spain E stays mainly C in the plain"
let scan = Scanner(string: s)
scan.scanString("A.1 ", into: nil)
var r1 : NSString? = nil
scan.scanUpTo(" B.2 ", into: &r1)
scan.scanString("B.2 ", into: nil)
var r2 : NSString? = nil
scan.scanUpTo(" E ", into: &r2)
scan.scanString("E ", into: nil)
var r3 : NSString? = nil
scan.scanUpTo(" C ", into: &r3)
scan.scanString("C ", into: nil)
var r4 : NSString? =
(scan.string as NSString).substring(from: scan.scanLocation) as NSString
r1 // the rain
r2 // in spain
r3 // stays mainly
r4 // in the plain

Refactored Solution In Swift

I've been studying for a coding exam by doing the HackerRank test cases, for the most part I've been doing well, but I get hung up on some easy cases and you all help me when I can't see the solution. I'm working on this problem:
https://www.hackerrank.com/challenges/ctci-ransom-note
A kidnapper wrote a ransom note but is worried it will be traced back to him. He found a magazine and wants to know if he can cut out whole words from it and use them to create an untraceable replica of his ransom note. The words in his note are case-sensitive and he must use whole words available in the magazine, meaning he cannot use substrings or concatenation to create the words he needs.
Given the words in the magazine and the words in the ransom note, print Yes if he can replicate his ransom note exactly using whole words from the magazine; otherwise, print No.
Input Format
The first line contains two space-separated integers describing the respective values of (the number of words in the magazine) and (the number of words in the ransom note).
The second line contains space-separated strings denoting the words present in the magazine.
The third line contains space-separated strings denoting the words present in the ransom note.
Each word consists of English alphabetic letters (i.e., to and to ).
The words in the note and magazine are case-sensitive.
Output Format
Print Yes if he can use the magazine to create an untraceable replica of his ransom note; otherwise, print No.
Sample Input
6 4
give me one grand today night
give one grand today
Sample Output
Yes
Explanation
All four words needed to write an untraceable replica of the ransom note are present in the magazine, so we print Yes as our answer.
And here is my solution:
import Foundation
func main() -> String {
let v = readLine()!.components(separatedBy: " ").map{Int($0)!}
var a = [String](); var b = [String]()
if v[0] < v[1] { return "No"}
for i in 0 ..< 2 {
if i == 0 {
a = (readLine()!).components(separatedBy: " ")
} else { b = (readLine()!).components(separatedBy: " ") }
}
// Get list of elements that intersect in each array
let filtered = Set(a).intersection(Set(b))
// Map set to set of Boolean where true means set a has enough words to satisfy set b's needs
let checkB = filtered.map{ word in reduceSet(b, word: word) <= reduceSet(a, word: word) }
// If mapped set does not contain false, answer is Yes, else No
return !checkB.contains(false) ? "Yes" : "No"
}
func reduceSet(_ a: [String], word: String) -> Int {
return (a.reduce(0){ $0 + ($1 == word ? 1 : 0)})
}
print(main())
I always time out on three of the 20 test-cases with this solution. So the solution seems to solve all the test cases, but not within their required time constraints. These are great practice, but it's so extremely frustrating when you get stuck like this.
I should note that I use Sets and the Set(a).intersection(Set(b)) because when I tried mapping an array of Strings, half the test-cases timed out.
Any cleaner, or more efficient solutions will be greatly appreciated! Thank you!
Thanks to #Alexander - I was able to solve this issue using NSCountedSet instead of my custom reduce method. It's much cleaner and more efficient. Here is the solution:
import Foundation
func main() -> String {
let v = readLine()!.components(separatedBy: " ").map{Int($0)!}
var a = [String](); var b = [String]()
if v[0] < v[1] { return "No"}
for i in 0 ..< 2 {
if i == 0 {
a = (readLine()!).components(separatedBy: " ")
} else { b = (readLine()!).components(separatedBy: " ") }
}
let countA = NSCountedSet(array: a)
let countB = NSCountedSet(array: b)
let intersect = Set(a).intersection(Set(b))
let check = intersect.map{ countB.count(for: $0) <= countA.count(for: $0) }
return !check.contains(false) ? "Yes" : "No"
}
print(main())
Many thanks!
I took the leisure of making some improvements on your code. I put comments to explain the changes:
import Foundation
func main() -> String {
// Give more meaningful variable names
let firstLine = readLine()!.components(separatedBy: " ").map{Int($0)!}
let (magazineWordCount, ransomNoteWordCount) = (firstLine[0], firstLine[1])
// a guard reads more like an assertion, stating the affirmative, as opposed to denying the negation.
// it also
guard magazineWordCount > ransomNoteWordCount else { return "No" }
// Don't use a for loop if it only does 2 iterations, which are themselves hardcoded in.
// Just write the statements in order.
let magazineWords = readLine()!.components(separatedBy: " ")
let ransomNoteWords = readLine()!.components(separatedBy: " ") //You don't need ( ) around readLine()!
let magazineWordCounts = NSCountedSet(array: magazineWords)
let ransomNoteWordCounts = NSCountedSet(array: ransomNoteWords)
// intersect is a verb. you're looking for the noun, "intersection"
// let intersection = Set(a).intersection(Set(b))
// let check = intersect.map{ countB.count(for: $0) <= countA.count(for: $0) }
// You don't actually care for the intersection of the two sets.
// You only need to worry about exactly the set of words that
// exists in the ransom note. Just check them directly.
let hasWordWithShortage = ransomNoteWordCounts.contains(where: { word in
magazineWordCounts.count(for: word) < ransomNoteWordCounts.count(for: word)
})
// Don't negate the condition of a conditional expression. Just flip the order of the last 2 operands.
return hasWordWithShortage ? "No" : "Yes"
}
print(main())
with the comments removed:
import Foundation
func main() -> String {
let firstLine = readLine()!.components(separatedBy: " ").map{Int($0)!}
let (magazineWordCount, ransomNoteWordCount) = (firstLine[0], firstLine[1])
guard magazineWordCount > ransomNoteWordCount else { return "No" }
let magazineWords = readLine()!.components(separatedBy: " ")
let ransomNoteWords = readLine()!.components(separatedBy: " ")
let magazineWordCounts = NSCountedSet(array: magazineWords)
let ransomNoteWordCounts = NSCountedSet(array: ransomNoteWords)
let hasWordWithShortage = ransomNoteWordCounts.contains{ word in
magazineWordCounts.count(for: word) < ransomNoteWordCounts.count(for: word)
}
return hasWordWithShortage ? "No" : "Yes"
}
print(main())
It's simpler, and much easier to follow. :)

Split text into array while maintaining the punctuation in Swift

I want to split the text into an array, maintaining the punctuation separated by the rest of the words, so a string like:
Hello, I am Albert Einstein.
should turn into an array like this:
["Hello", ",", "I", "am", "Albert", "Einstein", "."]
I have tried with sting.components(separatedBy: CharacterSet.init(charactersIn: " ,;;:")) but this method deletes all punctuations, and returns an array like this:
["Hello", "I", "am", "Albert", "Einstein"]
So, how can I get an array like my first example?
It's not beautiful as solution but you can try with:
var str = "Hello, I am Albert Einstein."
var list = [String]()
var currentSubString = "";
//enumerate to get all characters including ".", ",", ";", " "
str.enumerateSubstrings(in: str.startIndex..<str.endIndex, options: String.EnumerationOptions.byComposedCharacterSequences) { (substring, substringRange, enclosingRange, value) in
if let _subString = substring {
if (!currentSubString.isEmpty &&
(_subString.compare(" ") == .orderedSame
|| _subString.compare(",") == .orderedSame
|| _subString.compare(".") == .orderedSame
|| _subString.compare(";") == .orderedSame
)
) {
//create word if see any of those character and currentSubString is not empty
list.append(currentSubString)
currentSubString = _subString.trimmingCharacters(in: CharacterSet.whitespaces )
} else {
//add to current sub string if current character is not space.
if (_subString.compare(" ") != .orderedSame) {
currentSubString += _subString
}
}
}
}
//last word
if (!currentSubString.isEmpty) {
list.append(currentSubString)
}
In Swift3:
var str = "Hello, I am Albert Einstein."
var list = [String]()
var currentSubString = "";
//enumerate to get all characters including ".", ",", ";", " "
str.enumerateSubstrings(in: str.startIndex..<str.endIndex, options: String.EnumerationOptions.byComposedCharacterSequences) { (substring, substringRange, enclosingRange, value) in
if let _subString = substring {
if (!currentSubString.isEmpty &&
(_subString.compare(" ") == .orderedSame
|| _subString.compare(",") == .orderedSame
|| _subString.compare(".") == .orderedSame
|| _subString.compare(";") == .orderedSame
)
) {
//create word if see any of those character and currentSubString is not empty
list.append(currentSubString)
currentSubString = _subString.trimmingCharacters(in: CharacterSet.whitespaces )
} else {
//add to current sub string if current character is not space.
if (_subString.compare(" ") != .orderedSame) {
currentSubString += _subString
}
}
}
}
//last word
if (!currentSubString.isEmpty) {
list.append(currentSubString)
}
The idea is to loop for all character and create word in same time. A word is a group of consecutive character that is not , ,, . or ;. So, during the creation of word in loop, we finish the current word if we see one of those character, and the current word in construction is not empty.
To break down steps with your input:
get H (not space nor other terminal character)
-> currentSubString = "H"
get e (not space nor other terminal character)
-> currentSubString = "He"
get l (not space nor other terminal character)
-> currentSubString = "Hel"
get l (not space nor other terminal character)
-> currentSubString = "Hell"
get o (not space nor other terminal character)
-> currentSubString = "Hello"
get . (is terminal character)
-> as currentSubString is not empty, add to list and restart the construction for next word, then list = ["Hello"]
-> currentSubString = "." (the reason that I used trimming is just to remove if I get this character. but for other terminal character, we have to keep for next word.
get (is space character)
-> as currentSubString is not empty, add to listand restart the construction -> list = ["Hello", "."]
-> currentSubString = "" (trimmed).
... and so on.
To explain from my comment... Think of regular expressions as a way to nicely find patterns within Strings. In your case, the pattern is words (groups of letters) with other possible symbols (punctuation marks) in between.
Take the regex in my comment (which I've expanded a bit here), for example: ([,\.\:\"])*([A-Za-z0-9\']*)([,\.\:\"])*
In there, we have 3 groups. The first searches for any symbols (such as a leading quotation mark). The second is searching for letters, numbers, and an apostrophe (because people like to concatenate words, like "I'm"). and the third group searches for any trailing punctuation marks.
Edit to note: groups in the above are denoted by parentheses ( and ), while the [ and ] brackets denote acceptable characters for a search. So, for example, [A-Z] says that all upper case letters from A-Z are acceptable. [A-Za-z] lets you get both upper and lower, while [A-Za-z0-9] includes all letters and numbers from 0-9. Granted, there are shorthand versions to writing this, but those you'll discover down the road.
So now we have a way to separate all the words and punctuation marks, now you need to actually use it, doing something along the lines of:
func find(value: NSString) throws -> [NSString] {
let regex = try NSRegularExpression(pattern: "([,\\.\\:\\\"])*([A-Za-z0-9\\']*)([,\\.\\:\\\"])*") // Notice you have to escape the values in code
let results = regex.matches(in: value, range: NSRange(location: 0, length: nsString.length))
return results.map({ value.substring(with: $0.range) }).filter({ $0 != nil })
}
That should give you each non-nil group found within the String value you supply to the method.
Granted, that last filter method may not be necessary, but I'm not familiar enough with how Swift handles regex to know for sure.
But that should definitely point you in the right direction...
Cheers~

Swift: Split String into sentences

I'm wondering how I can split a string containing several sentences into an array of the sentences.
I know about the split function but spliting by "." doesn't suite for all cases.
Is there something like mentioned in this answer
You can use NSLinguisticsTagger to identify SentenceTerminator tokens and then split into an array of strings from there.
I used this code and it worked great.
https://stackoverflow.com/a/57985302/10736184
let text = "My paragraph with weird punctuation like Nov. 17th."
var r = [Range<String.Index>]()
let t = text.linguisticTags(
in: text.startIndex..<text.endIndex,
scheme: NSLinguisticTagScheme.lexicalClass.rawValue,
tokenRanges: &r)
var result = [String]()
let ixs = t.enumerated().filter {
$0.1 == "SentenceTerminator"
}.map {r[$0.0].lowerBound}
var prev = text.startIndex
for ix in ixs {
let r = prev...ix
result.append(
text[r].trimmingCharacters(
in: NSCharacterSet.whitespaces))
prev = text.index(after: ix)
}
Where result will now be an array of sentence strings. Note that the sentence will have to be terminated with '?', '!', '.', etc to count. If you want to split on newlines as well, or other Lexical Classes, you can add
|| $0.1 == "ParagraphBreak"
after
$0.1 == "SentenceTerminator"
to do that.
If you are capable of using Apple's Foundation then solution could be quite straightforward.
import Foundation
var text = """
Let's split some text into sentences.
The text might include dates like Jan.13, 2020, words like S.A.E and numbers like 2.2 or $9,999.99 as well as emojis like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦! How do I split this?
"""
var sentences: [String] = []
text.enumerateSubstrings(in: text.startIndex..., options: [.localized, .bySentences]) { (tag, _, _, _) in
sentences.append(tag ?? "")
}
There are ways do it with pure Swift of course. Here is quick and dirty split:
let simpleText = """
This is a very simple text.
It doesn't include dates, abbreviations, and numbers, but it includes emojis like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦! How do I split this?
"""
let sentencesPureSwift = simpleText.split(omittingEmptySubsequences:true) { $0.isPunctuation && !Set("',").contains($0)}
It could be refined with reduce().
Take a look on this link :
How to create String split extension with regex in Swift?
it shows how to combine regex and componentsSeparatedByString.
Try this:-
var myString : NSString = β€œThis is a test”
var myWords: NSArray = myString.componentsSeparatedByString(β€œ β€œ)
//myWords is now: ["This", "is", "a", "test"]

Resources