Find unique values in a Swift Array - arrays

I am building a project that tells me the unique words in a piece of text.
I have my orginal string scriptTextView which I have added each word into the array scriptEachWordInArray
I would now like to create an array called scriptUniqueWords which only includes words that appear once (in other words are unique) in scriptEachWordInArray
So I'd like my scriptUniqueWords array to equal = ["Silent","Holy"] as a result.
I don't want to create an array without duplicates but an array that has only values that appeared once in the first place.
var scriptTextView = "Silent Night Holy Night"
var scriptEachWordInArray = ["Silent", "night", "Holy", "night"]
var scriptUniqueWords = [String]()
for i in 0..<scriptEachWordInArray.count {
if scriptTextView.components(separatedBy: "\(scriptEachWordInArray[i]) ").count == 1 {
scriptUniqueWords.append(scriptEachWordInArray[i])
print("Unique word \(scriptEachWordInArray[i])")}
}

You can use NSCountedSet
let text = "Silent Night Holy Night"
let words = text.lowercased().components(separatedBy: " ")
let countedSet = NSCountedSet(array: words)
let singleOccurrencies = countedSet.filter { countedSet.count(for: $0) == 1 }.flatMap { $0 as? String }
Now singleOccurrencies contains ["holy", "silent"]

Swift
lets try It.
let array = ["1", "1", "2", "2", "3", "3"]
let unique = Array(Set(array))
// ["1", "2", "3"]

Filtering out unique words without preserving order
As another alternative to NSCountedSet, you could use a dictionary to count the the number of occurrences of each word, and filter out those that only occur once:
let scriptEachWordInArray = ["Silent", "night", "Holy", "night"]
var freqs: [String: Int] = [:]
scriptEachWordInArray.forEach { freqs[$0] = (freqs[$0] ?? 0) + 1 }
let scriptUniqueWords = freqs.flatMap { $0.1 == 1 ? $0.0 : nil }
print(scriptUniqueWords) // ["Holy", "Silent"]
This solution, however (as well as the one using NSCountedSet), will not preserve the order of the original array, since a dictionary as well as NSCountedSet is an unordered collection.
Filtering out unique words while preserving order
If you'd like to preserve the order from the original array (removing element which appear more than once), you could count the frequencies of each word, but store it in a (String, Int) tuple array rather than a dictionary.
Making use of the Collection extension from this Q&A
extension Collection where Iterator.Element: Hashable {
var frequencies: [(Iterator.Element, Int)] {
var seen: [Iterator.Element: Int] = [:]
var frequencies: [(Iterator.Element, Int)] = []
forEach {
if let idx = seen[$0] {
frequencies[idx].1 += 1
}
else {
seen[$0] = frequencies.count
frequencies.append(($0, 1))
}
}
return frequencies
}
}
// or, briefer but worse at showing intent
extension Collection where Iterator.Element: Hashable {
var frequencies: [(Iterator.Element, Int)] {
var seen: [Iterator.Element: Int] = [:]
var frequencies: [(Iterator.Element, Int)] = []
for elem in self {
seen[elem].map { frequencies[$0].1 += 1 } ?? {
seen[elem] = frequencies.count
return frequencies.append((elem, 1))
}()
}
return frequencies
}
}
... you may filter out the unique words of your array (while preserving order) as
let scriptUniqueWords = scriptEachWordInArray.frequencies
.flatMap { $0.1 == 1 ? $0.0 : nil }
print(scriptUniqueWords) // ["Silent", "Holy"]

you can filter the values that are already contained in the array:
let newArray = array.filter { !array.contains($0) }

Related

How to subtract one array by another array in Swift?

I want to subtract array1 by array2
Example:
var array1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
var array2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
I want Output:
["to","sun"]
What I am trying so far:
let reuslt = array1.filter { ! array2.contains($0) }
Output:
["sun"]
it's checking to contain a matching item removing all items if it matches but I want to remove one for one.
Just do it on the computer the way you would do it in your brain. Loop through array2 (not array1). For each element of array2, if that element has a firstIndex in array1, remove the element at that index from array1.
for word in array2 {
if let index = array1.firstIndex(of: word) {
array1.remove(at: index)
}
}
What you effectively want to do is this for loop
for item2 in array2 {
for i in 0..<array1.count {
if item2 == array1[i] {
array1.remove(at: i)
break
}
}
}
Filter works in exactly this way except it doesn't break on the first item but continues to remove all items.
You can also put this into a one liner like this with map instead of filter: array2.map({guard let i = array1.firstIndex(of: $0) else {return}; array1.remove(at: i)})
Like above answers, it is ok for a loop contains findIndex and remove from array.
But in other world, I think if the array is too large, the complexity of firstIndex(of:) and remove(at:) cause time and CPU too much for this task - Heat of device can raise a lots too. You can minimize it by using dictionary.
This is an another approach:
func findTheDiff(_ compareArr: [String], _ desArr: [String]) -> [String] {
var resultArr : [String] = []
var dict : [String: Int] = [:]
for word in compareArr {
if dict[word] == nil {
dict[word] = 1
} else {
dict[word] = dict[word]! + 1
}
}
for checkWord in desArr {
if dict[checkWord] != nil && dict[checkWord]! > 0 {
dict[checkWord] = dict[checkWord]! - 1
continue
}
resultArr.append(checkWord)
}
return resultArr
}
Usage:
var array1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
var array2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
var result = self.findTheDiff(array2, array1)
print(result) // ["to", "sun"]
You can find the complexity of firstIndex, remove(at:) below:
https://developer.apple.com/documentation/swift/array/firstindex(of:)
https://developer.apple.com/documentation/swift/array/remove(at:)-1p2pj
#Thang Phi's answer has the right idea. This is not different, but it works with a level of abstraction that incorporates the "counted set" idea for which Swift doesn't yet provide a built-in type:
import OrderedCollections
public extension Sequence where Element: Hashable {
/// A version of this sequence without the earliest occurrences of all `elementsToRemove`.
///
/// If `elementsToRemove` contains multiple equivalent values,
/// that many of the earliest occurrences will be filtered out.
func filteringOutEarliestOccurrences(from elementsToRemove: some Sequence<Element>) -> some Sequence<Element> {
var elementCounts = Dictionary(bucketing: elementsToRemove)
return lazy.filter {
do {
try elementCounts.remove(countedSetElement: $0)
return false
} catch {
return true
}
}
}
}
public extension Dictionary where Value == Int {
/// Create "buckets" from a sequence of keys,
/// such as might be used for a histogram.
/// - Note: This can be used for a "counted set".
#inlinable init(bucketing unbucketedKeys: some Sequence<Key>) {
self.init(zip(unbucketedKeys, 1), uniquingKeysWith: +)
}
/// Treating this dictionary as a "counted set", reduce the element's value by 1.
/// - Throws: If `countedSetElement` is not a key.
#inlinable mutating func remove(countedSetElement: Key) throws {
guard let count = self[countedSetElement] else { throw AnyError() }
self[countedSetElement] = count == 1 ? nil : count - 1
}
}
/// `zip` a sequence with a single value, instead of another sequence.
#inlinable public func zip<Sequence: Swift.Sequence, Constant>(
_ sequence: Sequence, _ constant: Constant
) -> some Swift.Sequence<(Sequence.Element, Constant)> {
zip(sequence, **ModuleName**.sequence(constant))
}
/// An infinite sequence of a single value.
#inlinable public func sequence<Element>(_ element: Element) -> some Sequence<Element> {
let getSelf: (Element) -> Element = \.self
return sequence(first: element, next: getSelf)
}
/// A nondescript error.
public struct AnyError: Error & Equatable {
public init() { }
}
Your example seems a little confusing / incomplete on the output. But sounds like you could do something like this:
extension Array where Element: Hashable {
func difference(from other: [Element]) -> [Element] {
let thisSet = Set(self)
let otherSet = Set(other)
return Array(thisSet.symmetricDifference(otherSet))
}
}
let names1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
let names2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
let difference = names1.difference(from: names2)
print(Array(difference)) // ["sun", "moon"]
Using an extension will of course make this code available to all of your arrays in your project. Since we convert the arrays into Sets, duplicates are removed, which may be a issue in your use case.
This Array extension was taken form: https://www.hackingwithswift.com/example-code/language/how-to-find-the-difference-between-two-arrays A vital resoruce for all things swift, especially SwiftUI.
Simple subtraction method that works for Equatable types including String.
extension Array where Element: Equatable {
func subtracting(_ array: Array<Element>) -> Array<Element> {
var result: Array<Element> = []
var toSub = array
for i in self {
if let index = toSub.firstIndex(of: i) {
toSub.remove(at: index)
continue
}
else {
result.append(i)
}
}
return result
}
}
let first = [1, 1, 2, 3, 3, 5, 6, 7, 7]
let second = [2, 2, 3, 4, 4, 5, 5, 6]
let result = first.subtracting(second)
//print result
//[1, 1, 3, 7, 7]

Filtering Dictionary with an array of random Ints to make a new dict

So I have this method to get an array of random ints between 1-9, a random number of times between 1 and 7.
let n = arc4random_uniform(7) + 1
var arr: [UInt32] = []
for _ in 0 ... n {
var temp = arc4random_uniform(9) + 1
while arr.contains(temp) {
temp = arc4random_uniform(9) + 1
}
print(temp)
arr.append(temp)
}
print(arr)
So that gets me an array like [1,4,2] or [5,7,3,4,6]. And I have a method to turn another array of strings into a enumerated dictionary.
var someArray: [String] = ["War", "Peanuts", "Cats", "Dogs", "Nova", "Bears", "Pigs", "Packers", "Mango", "Turkey"]
extension Collection {
var indexedDictionary: [Int: Element] {
return enumerated().reduce(into: [:]) { $0[$1.offset] = $1.element }
}
}
let dict1 = someArray.indexedDictionary
print(dict1)
giving me the indexed dictionary
[1:"War", 2:"Peanuts",..etc]
MY question is using the Ints of the random array how do I create a new dictionary that only includes those keys and their values?
So for example if arr = [3,1,5]
how do I get a new dictionary of
[3:"dogs", 1:"Peanuts",5:"Bears"].
This should do it:
let finalDict = dict1.filter { arr.contains($0.key) }
Update:
You can even go a step further and skip the whole strings to array mapping. So remove
extension Collection {
var indexedDictionary: [Int: Element] {
return enumerated().reduce(into: [:]) { $0[$1.offset] = $1.element }
}
}
let dict1 = someArray.indexedDictionary
print(dict1)
and just use this:
Swift 4:
let finalArray = someArray.enumerated().flatMap { arr.contains($0.offset) ? $0.element : nil }
Swift 4.1:
let finalArray = someArray.enumerated().compactMap { arr.contains($0.offset) ? $0.element : nil }
Update 2:
If you need a dictionary and not an array in the end use this:
Swift 4:
let finalDict = someArray.enumerated().flatMap { randomInts.contains($0.offset) ? ($0.offset, $0.element) : nil }.reduce(into: [:]) { $0[$1.0] = $1.1 }
Swift 4.1:
let finalDict = someArray.enumerated().compactMap { randomInts.contains($0.offset) ? ($0.offset, $0.element) : nil }.reduce(into: [:]) { $0[$1.0] = $1.1 }

How to group items from array that match, into another array?

For example say I have an array like so:
var someArray = ["1", "1", "2"]
I need to put this into two arrays that look like:
["1","1"]
["2"]
How can I go about this?
Any help would be great!
Use Dictionary initializer init(grouping:by:)
Then just get arrays by accessing values property.
Example:
let dic = Dictionary(grouping: someArray) { $0 }
let values = Array(dic.values)
print(values)
Result:
[["2"], ["1", "1"]]
Here are some facts (the upvote and answer should go to #kirander)
With #kirander method's is using the Dictionary to map the objects in a O(N) runtime and O(N) memory.
The other solutions are mostly running in O(N*N) runtime and O(N) memory. Because of this, grouping a random array of 1000 items will take: 0.07s with #kirander solution and 34s. with other solutions.
func benchmark(_ title:String, code: ()->()) {
let startTime = CFAbsoluteTimeGetCurrent()
code()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
print("Time elapsed for \(title): \(timeElapsed) s.")
}
var array:[String] = []
for _ in 0...1000 {
array.append("\(Int(arc4random_uniform(10)))")
}
// #kirander solution 0.07s
benchmark("Dictionary", code: {
let dic = Dictionary(grouping: array, by: { $0 })
let values = Array(dic.values)
})
// #Bruno solution ~34s
benchmark("Array", code: {
var resultingArrays = [[String]]()
for value in array {
let ar = array.filter({ $0 == value })
if !resultingArrays.contains(where: {$0 == ar}) {
resultingArrays.append(ar)
}
}
})
You could try something like this:
var someArray = ["1", "1", "2"]
var resultingArrays = [[String]]()
for value in someArray {
let array = someArray.filter({ $0 == value })
if !resultingArrays.contains(where: {$0 == array}) {
resultingArrays.append(array)
}
}
You can try this one :
let arrM = ["1","3","4","6","1","1","3"]
let arrSrtd = Array(Set(arrM))
for ele in arrSrtd{
let a = arrM.filter( {$0 == ele})
print(a)
}

Sort an array of dictionaries by a certain key value (Swift 3)

I was looking for a way to sort an array of invoice dictionaries based on invoice ID (which is a value of one of the keys in the dictionary). I couldn't find a solution that worked as well as I would've liked so I wrote out a function which works well for me and I thought I'd share it with the community.
It's pretty simple, you just pass in the array, pass in the key to sort it by and it returns the array sorted.
let testArray: Array<Dictionary<String, String>> = sortDictArrayByKey(arr: someUnsortedArray, key: keyToSort)
func sortDictArrayByKey(arr: Array<Dictionary<String, String>>, key: String) -> Array<Dictionary<String, String>>{
var keyArray = Array<String>()
var usedKeys = Array<String>()
for object in arr{
keyArray.append(object[key]!)
}
keyArray.sort(by: <)
var newArray = Array<Dictionary<String, String>>()
for keyVal in keyArray{
// Check if we've already seen this entry, if so, skip it
if usedKeys.contains(keyVal){
continue
}
usedKeys.append(keyVal)
// Check for duplicate entries
var tempArray = Array<Dictionary<String, String>>()
for object in arr{
if object[key] == keyVal{
tempArray.append(object)
}
}
for item in tempArray{
newArray.append(item)
}
tempArray.removeAll()
}
return newArray
}
Extension
You can created an extension available for Arrays of Dictionaries where both the Key and the Value are String(s).
extension Array where Element == [String:String] {
func sorted(by key: String) -> [[String:String]] {
return sorted { $0[key] ?? "" < $1[key] ?? "" }
}
}
Example
let crew = [
["Name":"Spook"],
["Name":"McCoy"],
["Name":"Kirk"]
]
crew.sorted(by: "Name")
// [["Name": "Kirk"], ["Name": "McCoy"], ["Name": "Spook"]]

Reduce a string to a dictionary in Swift

What woudl be a simple way to reduce a string like AAA:111;BBB:222;333;444;CCC:555 to a dictionary in Swift. I have the following code:
var str = "AAA:111;BBB:222;333;444;CCC:555"
var astr = str.componentsSeparatedByString(";").map { (element) -> [String:String] in
var elements = element.componentsSeparatedByString(":")
if elements.count < 2 {
elements.insert("N/A", atIndex: 0)
}
return [elements[0]:elements[1]]
}
The code above produces an Array of Dictionaries:
[["A": "111"], ["BBB": "222"], ["UKW": "333"], ["UKW": "444"], ["CCC": "555"]]
I want it to produce
["A": "111", "BBB": "222", "UKW": "333", "UKW": "444", "CCC": "555"]
but no mater what I try, since i call the map function on an Array it seems impossible to convert the nature of the map function's result.
NOTE: The dictionary in string format is described as either having KEY:VALUE; format or VALUE; format, in which case the mapping function will add the "N/A" as being the key of the unnamed value.
Any help on this matter is greatly appreciated.
Your map produces an array of dictionaries. When you want to combine them into 1, that's a perfect job for reduce:
func + <K,V>(lhs: Dictionary<K,V>, rhs: Dictionary<K,V>) -> Dictionary<K,V> {
var result = Dictionary<K,V>()
for (key, value) in lhs {
result[key] = value
}
for (key, value) in rhs {
result[key] = value
}
return result
}
var str = "AAA:111;BBB:222;333;444;CCC:555"
var astr = str
.componentsSeparatedByString(";")
.reduce([String: String]()) {
aggregate, element in
var elements = element.componentsSeparatedByString(":")
if elements.count < 2 {
elements.insert("N/A", atIndex: 0)
}
return aggregate + [elements[0]:elements[1]]
}
print(astr)
Swift has no default operator to "combine" two Dictionaries so you have to define one. Note that the + here is not commutative: dictA + dictB != dictB + dictA. If a key exist in both dictionaries, the value from the second dictionary will be used.
This is a work for reduce:
let str = "AAA:111;BBB:222;333;444;CCC:555"
let keyValueStrings = str.componentsSeparatedByString(";")
let dictionary = keyValueStrings.reduce([String: String]()) {
aggregate, element in
var newAggregate = aggregate
let elements = element.componentsSeparatedByString(":")
let key = elements[0]
// replace nil with the value you want to use if there is no value
let value = (elements.count > 1) ? elements[1] : nil
newAggregate[key] = value
return newAggregate
}
print(dictionary)
You can also make aggregate mutable directly:
let dictionary = keyValueStrings.reduce([String: String]()) {
(var aggregate: [String: String], element: String) -> [String: String] in
let elements = element.componentsSeparatedByString(":")
let key = elements[0]
// replace nil with the value you want to use if there is no value
let value = (elements.count > 1) ? elements[1] : nil
aggregate[key] = value
return aggregate
}
This is a functional approach, but you can achieve the same using a for iteration.
The reason this is happening is because map can only return arrays. If you are using this method to parse your string, then you need to convert it to a dictionary after.
var newDict = [String:String]()
for x in astr {
for (i, j) in x {
newDict[i] = j
}
}
The current issue with your code is that map function iterates over array containing [["key:value"],["key:value"]..] and you separate it again. But it returns ["key":"value"] which you then add to your array.
Instead you can add elements[0]:elements[1] directly to a locally kept variable which will fix your problem. Something like
finalVariable[elements[0]] = elements[1]

Resources