I have string array in Swift 2:
var myList : [String] = []
And I have dynamic strings inside and I explode them with * character myList examples:
print(myList[0]) output = 2018-04-05*type2*namea
print(myList[1]) output = 2018-04-05*type2*nameb
print(myList[2]) output = 2018-04-05*type3*nameb
print(myList[3]) output = 2018-04-06*type3*named
I want to delete objects have type3 ones in myList:
IF IN same date AND same name AND have type2 ones
Must be my strings like that:
print(myList[0]) output = 2018-04-05*type2*namea
print(myList[1]) output = 2018-04-05*type2*nameb
print(myList[2]) output = 2018-04-06*type3*named
This item below must be deleted:
print(myList[2]) output = 2018-04-05*type3*nameb
I want to delete type3 ones in myList if before have type2 with same date and same name basically.
Explain:
2018-04-05*type2*nameb and 2018-04-05*type3*nameb, have same date and same name but 2018-04-05*type3*nameb before have type2(2018-04-05*type2*nameb) ? so 2018-04-05*type3*nameb line must be delete
How can I do it?
This playground code will do what you want:
//: Playground - noun: a place where people can play
import UIKit
let myList = ["2018-04-05*type2*namea",
"2018-04-05*type2*nameb",
"2018-04-05*type3*nameb",
"2018-04-06*type3*named"]
//Define a class that lets us map from a string to a date, type, and name string
class ListEntry {
let fullString: String
//define lazy vars for all the substrings
lazy var subStrings: [Substring] = fullString.split(separator: "*")
lazy var dateString = subStrings[0]
lazy var typeString = subStrings[1]
lazy var nameString = subStrings[2]
//Create a failable initializer that takes a full string as input
//and tries to break it into exactly 3 substrings
//using the "*" sparator
init?(fullString: String) {
self.fullString = fullString
if subStrings.count != 3 { return nil }
}
}
print("---Input:---")
myList.forEach { print($0) }
print("------------")
//Map our array of strings to an array of ListEntry objects
let items = myList.compactMap { ListEntry(fullString: $0) }
//Create an output array
var output: [String] = []
//Loop through each item in the array of ListEntry objects, getting an index for each
for (index,item) in items.enumerated() {
//If this is the first item, or it dosn't have type == "type3", add it to the output
guard index > 0,
item.typeString == "type3" else {
print("Adding item", item.fullString)
output.append(item.fullString)
continue
}
let previous = items[index-1]
/*
Add this item if
-the previous type isn't "type2"
-the previous item's date doesn't match this one
-the previous item's name doesn't match this one
*/
guard previous.typeString == "type2",
item.dateString == previous.dateString,
item.nameString == previous.nameString else {
print("Adding item", item.fullString)
output.append(item.fullString)
continue
}
print("Skipping item ", item.fullString)
}
print("\n---Output:---")
output.forEach { print($0) }
The output of the code above is:
---Input:---
2018-04-05*type2*namea
2018-04-05*type2*nameb
2018-04-05*type3*nameb
2018-04-06*type3*named
------------
Adding item 2018-04-05*type2*namea
Adding item 2018-04-05*type2*nameb
Skipping item 2018-04-05*type3*nameb
Adding item 2018-04-06*type3*named
---Output:---
2018-04-05*type2*namea
2018-04-05*type2*nameb
2018-04-06*type3*named
I'll start you off with a simple (albeit hack-ish) approach:
let myList = ["2018-04-05*type2*namea", "2018-04-05*type2*nameb", "2018-04-05*type3*nameb", "2018-04-06*type3*named"]
Define the function:
func swapLastTwoComps(_ s: String) -> String {
let parts = s.split(separator: "*")
return [parts[0], parts[2], parts[1]].joined(separator: "*")
}
Now if you do
let myListS = myList.map {swapLastTwoComps($0)}.sorted()
you get
["2018-04-05*namea*type2", "2018-04-05*nameb*type2", "2018-04-05*nameb*type3", "2018-04-06*named*type3"]
i.e. the sort has left strings to be removed adjacent and to the right of their equivalent, so now you can easily loop through the array and remove the strings you want (because you only need to compare each String's prefix with the String immediately to its left to determine whether it should be removed).
Once you've done that, map swapLastTwoComps over the final array again to restore the strings to their previous format.
Related
I have an array that I populate from firestore that uses a struct. Is there a way to count the number of times there is a matching string for the productName var.
This is my struct...
struct PlayerStock: Codable, Identifiable {
#DocumentID var id: String?
var productName: String
var qty: Int
var saleUID: String
var totalPrice: Int
var uid: String
var unitPrice: Int
}
This is what's in my VC, I populate this from firestore and then want to count matching strings in productName
var playerStock: [PlayerStock] = []
Is there a way to do this without using a for loop?
Strings I'd like to count in productName include "smartphone" or "laptop" I want to store the matching total count as an int like this:
var smartphoneTotal =
var laptopTotal =
etc etc..
I've tried using filters and compact map but can't find anything that works, I think its because the array is multidimensional or because its using a dictionary?
Pretty noob here so any help appreciated!
First group the array by productName
let groupedProducts = Dictionary.init(grouping: playerStock, by: \.productName)
you'll get
["smartphone":[PlayerStock(..), PlayerStock(..), PlayerStock(..)],
"laptop":[PlayerStock(..), PlayerStock(..)]
then map the values to their amount of items
.mapValues(\.count)
The result is
["smartphone":3, "laptop":2]
If you want to use filter, something like this should work with your struct:
var laptopTotal = playerStock.filter { $0.productName == "laptop" }.count
This may help
let wordsToFind = ["smartphone", "laptop"]
var foundCounts: [String: Int] = [:]
for p in playerStock {
for word in wordsToFind {
if p.name.contains(word) {
foundCounts[word] = foundCounts[word, default: 0] + 1
}
}
}
foundCounts
If you really want a functional "no for-loops" version, and if you mean you want to find things that contain your search terms, then:
let wordsToFind = ["smartphone", "laptop"]
let founds = wordsToFind.map { word -> (String, Int) in
playerStock.reduce(("", 0)) { partialResult, player in
(word, partialResult.1 + (player.name.contains(word) ? 1 : 0))
}
}
You could use the higher order functions filter() or reduce(). #ShawnFrank already gave an answer using filter(). (voted.)
For a small number of items, there isn't a big difference between filter() and reduce(). For large datasets, though, filter creates a second array containing all the items that match the filter criteria. Arrays are value types, so they hold copies of the entries they contain. This would increase the memory footprint needed to do the counting. (You'd have the original array and a copy containing all the matching elements in memory).
The higher order function reduce() works differently. it takes a starting value (a total starting at 0 in our case) for the result, and a closure. The closure takes the current result, and an element from the array you are parsing. At runtime, the reduce() function calls your closure over and over, passing in each element from the array you are reducing. In the first call to the closure, it sends the closure the initial value for result (a zero total, in our case.) In each subsequent call to the closure, it passes the result of the previous call. (The running total, for our implementation.) The reduce() function returns the result returned by the last call to your closure.
You can use reduce to count the number of items that match a given test without having to build a temporary array. Below is a sample implementation using reduce(). Note that I tweaked your PlayerStock type to add default values for all the properties other than productName since I don't care about those.
// Define the PlayerStock type, but use default values for everything but `productName`
struct PlayerStock: Codable, Identifiable {
var id: String? = nil
var productName: String
var qty: Int = Int.random(in: 1...10)
var saleUID: String = ""
var totalPrice: Int = Int.random(in: 10...200)
var uid: String = ""
var unitPrice: Int = Int.random(in: 10...200)
}
// Create an array of test data
let players = [
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "CD Player"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "CD Player"),
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "boom box"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "smartphone"),
]
/// This is a function that counts and returns the number of PlayerStock items who's productName property matches a the string nameToFind.
/// If you pass in printResult = true, it logs its result for debugging.
/// - Parameter nameToFind: The `productName` to search for
/// - Parameter inArray: The array of `PlayerStock` items to search
/// - Parameter printResult: a debugging flag. If true, the function prints the count if items to the console. Defaults to `false`
/// - Returns: The number of `PlayerStock` items that have a `productName` == `nameToFind`
#discardableResult func countPlayers(nameToFind: String, inArray array: [PlayerStock], printResult: Bool = false) -> Int {
let count = array.reduce(0, { count, item in
item.productName == nameToFind ? count+1 : count
})
if printResult {
print("Found \(count) players with productName == \(nameToFind)")
}
return count
}
let smartphoneCount = countPlayers(nameToFind: "smartphone", inArray: players, printResult: true)
let laptopCount = countPlayers(nameToFind: "laptop", inArray: players, printResult: true)
let cdPlayerCount = countPlayers(nameToFind: "CD Player", inArray: players, printResult: true)
This sample code produces the following output:
Found 4 players with productName == smartphone
Found 3 players with productName == laptop
Found 2 players with productName == CD Player
fun main() {
val data = ArrayList<List<String>>()
data.add(listOf("32701", "First"))
data.add(listOf("32702", "Second"))
data.add(listOf("32702", "Second"))
data.add(listOf("32701", "First True"))
println(data.distinct())
}
Result :
[[32701, First], [32702, Second], [32701, First True]]
Question How about removing data [32701, First] and get new data with the same value ?
Expected :
[32702, Second], [32701, First True]]
The problem is that distinct() uses the equals methods and comparing the entirety of the list.
You could use distinctyBy { it.first() } if you can ensure lists wont be empty
Edit
In order to get latest value you can:
a) Reverse the list and then call distinctBy
yourList
.reversed() // Now latest values are first in the list
.distinctBy { it.first() } // first element of list holds the id
b) Associate the values into a map of Map<String, List<String>> by calling associateBy { it.first()} and getting the last value of the map by calling
val correctResults = map.values.map { valueList -> valueList.last() }
As a whole would look like:
yourList
.associateBy { it.first() }
.values
.map { valueList -> valueList.last() }
Be aware that any of these approaches IS NOT dealing with empty lists.
In order to deal with empty lists you could filter them out by just doing
val listsThatAreNotEmpty = yourList.filter { it.isNotEmpty() }
Use a combination of reversed and disinctBy:
fun main() {
val Data = ArrayList<List<String>>()
Data.add(listOf("32701", "First"))
Data.add(listOf("32702", "Second"))
Data.add(listOf("32702", "Second"))
Data.add(listOf("32701", "First True"))
println(Data.reversed().distinctBy{it[0]} )
// prints [[32701, First True], [32702, Second]]
}
You can reverse the result again to get the original relative order.
As mentioned by others, the use of listOf is sub-optimal, here is a cleaner version:
data class Item(val id: String, val text: String)
fun distinct(data : List<Item>) = data.reversed().distinctBy{it.id}
fun main() {
val data = listOf(
Item("32701", "First"),
Item("32702", "Second"),
Item("32702", "Second"),
Item("32701", "First True")
)
println(distinct(data) )
// [Item(id=32701, text=First True), Item(id=32702, text=Second)]
}
I have a text which should be saved in android string.xml resources. For this, I want to save it in a list of Data class.
Title1
Body1 .....
Title2
Body2......
Titles are surrounded with 2 newLines, I need to split titles from bodies. How to do this? Are there any regex patterns?
UPDATE:
if body contains more than 1 paragraphs:
Title1
Body1 .....
BodyContinuation
New Paragraph
Title2
Body2......
How to handle this case?
You may parse it in kotlin itself, it'll do it in O(n) steps similar to regex by consuming the line sequence.
Example:
fun parseTitle(str: String): Sequence<String> = sequence {
var emptyLines = 2
str.lineSequence().forEach {
when {
emptyLines > 1 -> {
yield(it)
emptyLines = 0
}
it.isNotEmpty() -> emptyLines = 0
else -> emptyLines++
}
}
}
val test =
"""
Title1
Body1 .....
Title2
Body2......
""".trimIndent()
println(parseTitle(test).toList()) // [Title1, Title2]
Edit:
If there are multiple paragraphes which contains any number of line breaks then the only constraint you can put to filter them out is by the size.
/**
* [str] is the input string, [maxLen] is maximum length until which line
* should be expected to be a title
*/
fun parseTitle(str: String, maxLen: Int): Sequence<String> =
str.lineSequence()
.filter { it.isNotEmpty() }
.mapNotNull { if(it.length <= maxLen) it else null }
// .toList() // to trigger terminal operation and collect the result into list
You can .split("\n\n") your string to get a list of string, then use .chunked(2) { ... } to group resulting list into pair of string, mapping each pair to your data class instance.
Example:
data class Item(val title: String, val body: String)
fun parseItems(input: String): List<Item> =
input.splitToSequence("\n\n")
.chunked(2) { (title, body) -> Item(title, body) }
.toList()
fun main(args: Array<String>) {
val testData = """
Title1
Body1 .....
Title2
Body2......
""".trimIndent()
println(parseItems(testData))
}
It outputs the following:
[Item(title=Title1, body=Body1 .....), Item(title=Title2, body=Body2......)]
You can play with it here: https://pl.kotl.in/8ix38By7m
i have a array of tuple like
var contactsname = [(String,String)]()//firstname,lastname
example = [(alex,joe),(catty,drling),(alex,fox),(asta,alex)]
i need to search elements which are matching the firstname or lastname and return those all elements matching the key
func searchElementsForkey(key:String)->[(string,String)]{ //code here }
searchElementsForKey("alex") = [(alex,joe),(alex,fox),(asta,alex)]
You can go like this.
var contactsname = [(String,String)]()//firstname,lastname
contactsname = [("alex","joe"),("catty","drling"),("alex","fox"),("asta","alex")]
let key = "alex"
If you want to exact match the search name either with First name or Last name
let filterArray = contactsname.filter { $0.0 == key || $0.1 == key }
If you want to check First name and Last name contains specific string for that you can use contains
let filterArray = contactsname.filter { $0.0.contains(key) || $0.1.contains(key) }
i have a struct array that i want "break up" into smaller arrays that can be called as needed or at least figure out how i can map the items needed off one text value.
the struct:
struct CollectionStruct {
var name : String
var description : String
var title : String
var image : PFFile
var id: String
}
and the array made from the struct
var collectionArray = [CollectionStruct]()
var i = 0
for item in collectionArray {
print(collectionArray[i].name)
i += 1
}
printing partArray[i].name gives the following result:
pk00_pt01
pk00_pt02
pk00_pt03
pk01_pt01
pk01_pt02
pk01_pt03
pk01_pt04
pk01_pt05
pk01_pt06
pk01_pt07
pk01_pt08
this is just some test values but there could be thousands of entries here so i wanted to filter the entire array just by the first 4 characters of [i].name i can achieve this by looping through as above but is this achievable using something like .map?
I wanted to filter the entire array just by the first 4 characters of
[i].name
You can achieve this by filtering the array based on the substring value of the name, as follows:
let filteredArray = collectionArray.filter {
$0.name.substring(to: $0.name.index($0.name.startIndex, offsetBy: 4)).lowercased() == "pk00"
// or instead of "pk00", add the first 4 characters you want to compare
}
filteredArray will be filled based on what is the compared string.
Hope this helped.
If you want to group all data automatically by their name prefix. You could use a reducer to generate a dictionary of grouped items. Something like this:
let groupedData = array.reduce([String: [String]]()) { (dictionary, myStruct) in
let grouper = myStruct.name.substring(to: myStruct.name.index(myStruct.name.startIndex, offsetBy: 4))
var newDictionart = dictionary
if let collectionStructs = newDictionart[grouper] {
newDictionart[grouper] = collectionStructs + [myStruct.name]
} else {
newDictionart[grouper] = [myStruct.name]
}
return newDictionart
}
This will produce a dictionary like this:
[
"pk00": ["pk00_pt01", "pk00_pt02", "pk00_pt03"],
"pk01": ["pk01_pt01", "pk01_pt02", "pk01_pt03", "pk01_pt04", "pk01_pt05", "pk01_pt06", "pk01_pt07"],
"pk02": ["pk02_pt08"]
]
Not sure if i am understanding you correctly but it sounds like you are looking for this...
To create a new array named partArray from an already existing array named collectionArray (that is of type CollectionStruct) you would do...
var partArray = collectionArray.map{$0.name}