I'm having this issue of trying to trying to recursively print out all subsets of the giving array of String(characters) using swift. The value is ("a1b2"). The output should have 4 subsets.
Currently stuck here:
func overall(string: String) {
helper(string: string, i: 0, slate: "")
}
func helper(string: String, i: Int, slate: String) {
var result = [Any]()
let word = Array(string)
var counter = i
if string.count == counter {
result.append(slate)
} else {
if word[i].isNumber {
counter += 1
helper(string: string, i: counter, slate: slate + String(word[i]))
} else if word[i].isLowercase {
counter += 1
helper(string: string, i: counter, slate: slate + String(word[i]).uppercased())
} else {
counter += 1
helper(string: string, i: counter, slate: slate + String(word[i]).lowercased())
}
}
}
overall(string: "a1b2")
I'm having issues creating a base case in the helper functions. Also I'm unsure if I'm using recursion properly. Could you please help with an explanation, it will be greatly appreciated.
I'm sure this is totally useless in general, but just for fun, here's an amusing recursion-free solution for the particular problem given, where we know the string is exactly four characters and we know that either uppercased or lowercased must be applied to each character:
let s = "a1b2"
let arr = Array(s).map(String.init)
var result : Set<String> = []
for i in 0b0000...0b1111 {
var tog = [Bool]()
for sh in 0...3 { tog.append(i & 1<<sh == 0) }
var word = ""
for ix in 0...3 {
let f = tog[ix] ? arr[ix].lowercased : arr[ix].uppercased
word = word + f()
}
result.insert(word)
}
print(result)
The OP clarified in comments that he wanted the case-variants of the original string, not "subsets" as originally stated
[Edit] I originally had a paragraph here about String.count, however, my memory must have been in error, because Apple's documentation does state that String.count is in fact the number of Characters, which is what we would all want it to be anyway. I hope my error didn't throw anyone too far off.
You don't need any counters. All you need is the first character, and recurse on the rest of the string.
The thing is, when you have a letter as your first you need to preprend both the upper and lower case variants to all of the strings returned by the recursive call.
The base case is at the end of the string, in which case you return an an array containing just the empty string.
Here's my implementation:
func caseVariants(of s: String) -> [String]
{
func caseVariants(of s: Substring) -> [String]
{
guard let c = s.first else { return [""] } // base case
let remainder = s[s.index(after: s.startIndex)...]
let remainderVariants = caseVariants(of: remainder)
var results: [String] = []
if c.isLetter
{
results.append(
contentsOf: remainderVariants.map {
"\(c.uppercased())" + $0
}
)
results.append(
contentsOf: remainderVariants.map {
"\(c.lowercased())" + $0
}
)
}
else
{
results.append(
contentsOf: remainderVariants.map { "\(c)" + $0 }
)
}
return results
}
return caseVariants(of: s[...]).sorted()
}
print("Case variants:")
for s in caseVariants(of: "a1b2") { print("\"\(s)\"") }
The output is:
Case variants:
"A1B2"
"A1b2"
"a1B2"
"a1b2"
[EDIT] in comments, OP asked what if .startIndex were disallowed (such as in an interview). While I think such a restriction is insane, there is an easy solution, and it's a one-line, quite reasonable change to my previous code. Change this line:
let remainder = s[s.index(after: s.startIndex)...]
to use .dropFirst()
let remainder = s.dropFirst()
If we look at the implementation of dropFirst in the Collection protocol in the standard library:
#inlinable
public __consuming func dropFirst(_ k: Int = 1) -> SubSequence {
_precondition(k >= 0, "Can't drop a negative number of elements from a collection")
let start = index(startIndex, offsetBy: k, limitedBy: endIndex) ?? endIndex
return self[start..<endIndex]
}
We see that the use of dropFirst will use the default value of 1 for k. In that case, when we've already checked that we're not at the end of the string, the line
let start = index(startIndex, offsetBy: k, limitedBy: endIndex) ?? endIndex
is equivalent to
let start = index(after: startIndex)
which means that the returned substring is
return self[index(after: startIndex)..<endIndex]
which is just the canonical way of saying:
return self[index(after: startIndex)...]
So a version using dropFirst() is identical to the original solution once inlining has done its thing.
Related
I want to create a function where an input of a string is taken in and the output returns a string of how many times the number is repeated. For example if my string is "1111223444" it will return "41221334", because there are four 1's, two 2's, one 3, and three 4's. So an input of "2234467" would return "2213241617". I am not sure if a dictionary would be the best way to implement it and I am really confused. I have started the function however don't know where to go from here. Any tips or resources will be helpful.
func stringOutput(input: String) -> String {
var result = ""
var lastCharacter: Character
var count = 0
var countDict: [String: Int] = [:]
for item in input {
countDict[item] as Character
}
return result
}
Your function computes the next term of the Look-and-say sequence. Here is an implementation (essentially taken from Leetcode 38: The “count-and-say” sequence on Code Review). Instead of traversing the string, we directly search for the next index of a character different from the current one. Neither the first nor the last run has be be treated specially:
extension String {
func lookAndSay() -> String {
var result = ""
var fromIndex = startIndex // Start of current run
while fromIndex != endIndex {
let char = self[fromIndex] // Current character
// Find start of next run
let toIndex = self[fromIndex...].firstIndex(where: { $0 != char }) ?? endIndex
// Compute length of run, and append it to the result
let len = distance(from: fromIndex, to: toIndex)
result += "\(len)\(char)"
// Continue with next run
fromIndex = toIndex
}
return result
}
}
print("1111223444".lookAndSay()) // 41221334
print("2234467".lookAndSay()) // 2213241617
Try the snippet below.
func stringOutput(input: String) -> String {
var result = ""
var lastKnownCharacter: Character? = nil
var lastKnownCharacterCount: Int = 0
for character in input {
if lastKnownCharacter == nil {
lastKnownCharacter = character
lastKnownCharacterCount = 1
} else if lastKnownCharacter == character {
lastKnownCharacterCount += 1
} else {
result.append("\(lastKnownCharacterCount)\(lastKnownCharacter!)")
lastKnownCharacter = character
lastKnownCharacterCount = 1
}
}
result.append("\(lastKnownCharacterCount)\(lastKnownCharacter!)")
return result
}
I'm working from a previous posting on AppCode called "Core Data Basics: Preload Data and Use Existing SQLite Database" located here: https://www.appcoda.com/core-data-preload-sqlite-database/
Within Simon Ng's posting is a function called parseCSV which does all the heavy lifting of scanning through a .csv and breaking it up into it's respective rows so that each row's elements can then be saved into their respective managedObjectContext in core data.
Unfortunately all of the code appears to be written in either Swift 1.0 or Swift 2.0 and I have been unable to understand the errors I'm getting in converting it into Swift 4.
I've made all of the changes suggested by Xcode with regards to "this" has been replaced with "that", with the final error telling me "Argument labels '(contentsOfURL:, encoding:, error:)' do not match any available overloads" which I have been unable to understand nor correct.
// https://www.appcoda.com/core-data-preload-sqlite-database/
func parseCSV (contentsOfURL: NSURL, encoding: String.Encoding, error: NSErrorPointer) -> [(name:String, detail:String, price: String)]? {
// Load the CSV file and parse it
let delimiter = ","
var items:[(name:String, detail:String, price: String)]?
if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {
items = []
let lines:[String] = content.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
for line in lines {
var values:[String] = []
if line != "" {
// For a line with double quotes
// we use NSScanner to perform the parsing
if line.range(of: "\"") != nil {
var textToScan:String = line
var value:NSString?
var textScanner:Scanner = Scanner(string: textToScan)
while textScanner.string != "" {
if (textScanner.string as NSString).substring(to: 1) == "\"" {
textScanner.scanLocation += 1
textScanner.scanUpTo("\"", into: &value)
textScanner.scanLocation += 1
} else {
textScanner.scanUpTo(delimiter, into: &value)
}
// Store the value into the values array
values.append(value! as String)
// Retrieve the unscanned remainder of the string
if textScanner.scanLocation < textScanner.string.count {
textToScan = (textScanner.string as NSString).substring(from: textScanner.scanLocation + 1)
} else {
textToScan = ""
}
textScanner = Scanner(string: textToScan)
}
// For a line without double quotes, we can simply separate the string
// by using the delimiter (e.g. comma)
} else {
values = line.components(separatedBy: delimiter)
}
// Put the values into the tuple and add it to the items array
let item = (name: values[0], detail: values[1], price: values[2])
items?.append(item)
}
}
}
return items
}
The 5th line:
if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {
is throwing the following error:
Argument labels '(contentsOfURL:, encoding:, error:)' do not match any available overloads
Which is beyond my understanding and skill level. I'm really just trying to find the best way of importing a comma separated .csv file into a core data object.
Any assistance would be appreciated. The original example by Simon Ng appears perfect for what I'm trying to achieve. It just hasn't been updated in a very long time.
First of all - you all are brilliant contributors and bloody fast at your intel. I'd like to thank all of you for answering so quickly. Here's where I ended up with that particular function in the latest Swift 5 syntax.
func parseCSV (contentsOfURL: NSURL, encoding: String.Encoding, error: NSErrorPointer) -> [(name:String, detail:String, price: String)]? {
// Load the CSV file and parse it
let delimiter = ","
var items:[(name:String, detail:String, price: String)]?
//if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {
if let content = try? String(contentsOf: contentsOfURL as URL, encoding: encoding) {
items = []
let lines:[String] = content.components(separatedBy: NSCharacterSet.newlines) as [String]
for line in lines {
var values:[String] = []
if line != "" {
// For a line with double quotes
// we use NSScanner to perform the parsing
if line.range(of: "\"") != nil {
var textToScan:String = line
var value:NSString?
var textScanner:Scanner = Scanner(string: textToScan)
while textScanner.string != "" {
if (textScanner.string as NSString).substring(to: 1) == "\"" {
textScanner.scanLocation += 1
textScanner.scanUpTo("\"", into: &value)
textScanner.scanLocation += 1
} else {
textScanner.scanUpTo(delimiter, into: &value)
}
// Store the value into the values array
values.append(value! as String)
// Retrieve the unscanned remainder of the string
if textScanner.scanLocation < textScanner.string.count {
textToScan = (textScanner.string as NSString).substring(from: textScanner.scanLocation + 1)
} else {
textToScan = ""
}
textScanner = Scanner(string: textToScan)
}
// For a line without double quotes, we can simply separate the string
// by using the delimiter (e.g. comma)
} else {
values = line.components(separatedBy: delimiter)
}
// Put the values into the tuple and add it to the items array
let item = (name: values[0], detail: values[1], price: values[2])
items?.append(item)
}
}
}
return items
}
As of Swift 3, that function has been changed to String(contentsOf:, encoding:) so you just need to modify the argument labels in code.
It's also worth mentioning, that this function will now throw so you will have to handle that. It wouldn't do any harm for you to take a look at this page on exception handling in Swift.
Because Scanner has been changed up in iOS 13 in ways that seem to be poorly explained, I rewrote this to work without it. For my application, the header row is of interest, so it's captured separately; if it's not meaningful then that part can be omitted.
The code starts with workingText which has been read from whatever file or URL is the source of the data.
var headers : [String] = []
var data : [[String]] = []
let workingLines = workingText.split{$0.isNewline}
if let headerLine = workingLines.first {
headers = parseCsvLine(ln: String(headerLine))
for ln in workingLines {
if ln != headerLine {
let fields = parseCsvLine(ln: String(ln))
data.append(fields)
}
}
}
print("-----------------------------")
print("Headers: \(headers)")
print("Data:")
for d in data {
print(d) // gives each data row its own printed row; print(data) has no line breaks anywhere + is hard to read
}
print("-----------------------------")
func parseCsvLine(ln: String) -> [String] {
// takes a line of a CSV file and returns the separated values
// so input of 'a,b,2' should return ["a","b","2"]
// or input of '"Houston, TX","Hello",5,"6,7"' should return ["Houston, TX","Hello","5","6,7"]
let delimiter = ","
let quote = "\""
var nextTerminator = delimiter
var andDiscardDelimiter = false
var currentValue = ""
var allValues : [String] = []
for char in ln {
let chr = String(char)
if chr == nextTerminator {
if andDiscardDelimiter {
// we've found the comma after a closing quote. No action required beyond clearing this flag.
andDiscardDelimiter = false
}
else {
// we've found the comma or closing quote terminating one value
allValues.append(currentValue)
currentValue = ""
}
nextTerminator = delimiter // either way, next thing we look for is the comma
} else if chr == quote {
// this is an OPENING quote, so clear currentValue (which should be nothing but maybe a single space):
currentValue = ""
nextTerminator = quote
andDiscardDelimiter = true
} else {
currentValue += chr
}
}
return allValues
}
I freely acknowledge that I probably use more conversions to String than those smarter than I am in the ways of Apple strings, substrings, scanners, and such would find necessary. Parsing a file of a few hundred rows x about a dozen columns, this approach seems to work fine; for something significantly larger, the extra overhead may start to matter.
An alternative is to use a library to do this. https://github.com/dehesa/CodableCSV supports this and has a list of other swift csv libraries too
With the new ways of handling string in Swift 4, I'm trying to wrap my head around how to write the equivalent of the Mid function from other languages (visual basic, etc), so that
let testString = "0123456"
print Mid(testString, 2,4) // "1234" (or "2345" would work too)
This question is the same idea, but everything there predates Swift 4. If the answer there by jlert is still the best way to do things in Swift 4, that works, although it seems like so much has changed that the best practice to do this may have changed as well.
One way to do it is with a combination of dropFirst and prefix and then use String to convert the result back to String:
let testString = "0123456"
func mid(_ str: String, _ low: Int, _ count: Int) -> String {
return String(str.dropFirst(low).prefix(count))
}
print(mid(testString, 2,4)) // 2345
dropFirst and prefix are forgiving and won't crash if enough letters are not there. They will just give a truncated result. Depending on how you define the function, this is a perfectly acceptable implementation.
Another approach would be to use array subscripting. If you do it that way, you need to check inputs to avoid Array index out of range fatal errors:
func mid(_ str: String, _ low: Int, _ count: Int) -> String? {
guard low >= 0,
count >= 0,
low < str.count,
low + count <= str.count
else { return nil }
return String(Array(str)[low ..< low + count])
}
let testString = "abcdefghi"
if let result = mid(testString, 2, 4) {
print(result) // cdef
}
You can do:
extension String {
func mid(_ startOffset: Int, _ length: Int) -> Substring {
let start = index(startIndex, offsetBy: startOffset, limitedBy: endIndex) ?? endIndex
let end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex
return self[start ..< end]
}
}
let string = "0123456"
let result = string.mid(2, 4) // 2345
Note, this returns a Substring, which enjoys memory efficiency. But if you want a String, you can do :
let result = String(string.mid(2, 4))
(Or you can incorporate this in your mid function.)
I would take a different approach using String.Index and return a Substring instead of a new String object. You can also add precondition to restrict improper use of that method:
func mid(_ string: String, _ positon: Int, length: Int) -> Substring {
precondition(positon < string.count, "invalid position")
precondition(positon + length <= string.count, "invalid substring length")
let lower = string.index(string.startIndex, offsetBy: positon)
let upper = string.index(lower, offsetBy: length)
return string[lower..<upper]
}
let testString = "0123456"
mid(testString, 2, length: 4) // "2345"
Another option would be creating that method as a string extension:
extension String {
func mid(_ positon: Int, length: Int) -> Substring {
precondition(positon < count, "invalid position")
precondition(positon + length <= count, "invalid substring length")
let lower = index(startIndex, offsetBy: positon)
let upper = index(lower, offsetBy: length)
return self[lower..<upper]
}
}
let testString = "0123456"
testString.mid(2, length: 4) // "2345"
iOS 14, Swift 5... same thing as vacawama excellent answer, but as an extension, so it's even less code :)
extension String {
func mid(_ low: Int, _ count: Int) -> String {
return String(self.dropFirst(low).prefix(count))
}
}
Use it like this.
let pString = "01234567"
for i in 0 ..< 8 {
print("\(i) \(pString.mid(i,1)")
}
Prints out your string to the console, one character at a time
I wrote a method that removes all 2 duplicate characters in String, for example
I need delete only char that contains twice, for example
"bndkss" -> "bndk"
"nnmmhj" - > "hj"
"aaabbaac" -> "ac
"abba" -> ""
I wrote on objc and everything works, but Swift is not working, help please, where did I go wrong?
override func viewDidLoad() {
super.viewDidLoad()
let string = "baab"
print("before: \(string)")
let stringAfter = checkString(string: string)
print("after: \(stringAfter)")
}
func checkString(string : String) -> String {
var tempString = string
for (index, element) in string.characters.enumerated() {
for (index2, element2) in string.characters.enumerated() {
if element == element2 && index != index2 {
if index > index2 {
tempString.remove(at: tempString.index(tempString.startIndex, offsetBy: index))
tempString.remove(at: tempString.index(tempString.startIndex, offsetBy: index2))
} else {
tempString.remove(at: tempString.index(tempString.startIndex, offsetBy: index2))
tempString.remove(at: tempString.index(tempString.startIndex, offsetBy: index))
}
if tempString.characters.count < 1 {
return ""
} else {
checkString(string: tempString)
}
} else {
if index == tempString.characters.count - 1 && index2 == tempString.characters.count - 1 {
return tempString
}
}
}
}
return ""
}
Updates:
just need
return checkString(string: tempString)
instead
checkString(string: tempString)
There are two problems in your code, such as
After removing characters in tempString, the indices index and index2 to long refer to the original characters in tempString.
Wrong characters are removed as a consequence.
You call checkString() recursively but discard the result.
Update: As you already noticed in the meantime, return checkString(string: tempString) solves these problems.
Here is an alternative implementation. The idea is to use a dictionary
to remember where a character has been seen last, and an index set
which keeps track of the positions of the characters which are to
be preserved. Instead of two nested loops and recursion, two "simple"
loops are used here, plus the cost of the dictionary and set operations.
func removeDuplicateCharacters(string: String) -> String {
var seen = [Character: Int]()
var keep = IndexSet(integersIn: 0..<string.characters.count)
for (idx, c) in string.characters.enumerated() {
if let prevIndex = seen[c] {
keep.remove(prevIndex)
keep.remove(idx)
seen.removeValue(forKey: c)
} else {
seen[c] = idx
}
}
return String(keep.map { string[string.index(string.startIndex, offsetBy: $0)] })
}
Examples:
print(removeDuplicateCharacters(string: "bndkss")) // ""bndk"
print(removeDuplicateCharacters(string: "nnmmhj")) // "jh"
print(removeDuplicateCharacters(string: "abba")) // ""
print(removeDuplicateCharacters(string: "aaabbaac")) // "ac"
Martin wrote a much cleaner version than myself, but I worked on this for a little while so I figured I'd post it to show you another way it could have been accomplished.
func removeDuplicates(from original: String) -> String {
var originalString = original
var newString = ""
for character in originalString.characters {
if !newString.contains("\(character)") {
newString.append(character)
originalString = originalString.characters.filter { $0.description != "\(character)" }.map { "\($0)" }.joined(separator: "")
} else {
newString = newString.characters.filter { $0.description != "\(character)" }.map { "\($0)" }.joined(separator: "")
}
}
return newString
}
I am trying to solve the Palindrome Partitioning Question. You can find the question in https://leetcode.com/problems/palindrome-partitioning/.
And I came up with the solution:
func partition(_ s: String) -> [[String]] {
var result: [[String]] = []
func dfs(string: String, partiton: [String]) {
if string.characters.count == 0 {
result.append(partiton)
return
}
for length in 1...string.characters.count {
let endIndex = string.index(string.startIndex, offsetBy: length-1)
let part = string[string.startIndex...endIndex]
if isPalindrome(part) {
let leftPart = string[string.index(after: endIndex)..<string.endIndex]
print("string: \(string) part: \(part) leftpart: \(leftPart)")
dfs(string: leftPart, partiton: partiton + [part])
}
}
}
func isPalindrome(_ s: String) -> Bool {
if String(s.characters.reversed()) == s {
return true
} else {
return false
}
}
dfs(string: s, partiton: [])
return result
}
But the performance is Bad. Time Limit Exceeded.
But the same idea with Python implementation can pass:
def partition(self, s):
res = []
self.dfs(s, [], res)
return res
def dfs(self, s, path, res):
if not s:
res.append(path)
return
for i in range(1, len(s)+1):
if self.isPal(s[:i]):
self.dfs(s[i:], path+[s[:i]], res)
def isPal(self, s):
return s == s[::-1]
It make me wonder that how to improve the swift implementation and why the swift implementation is slower than python.
A Swift String is a collection of Characters, and a Character represents a single extended grapheme cluster, that can be one or more
Unicode scalars. That makes some index operations like "skip the first N characters" slow.
But the first improvement is to "short-circuit" the isPalindrome()
function. Instead of building the reversed string completely, compare
the character sequence with its reversed sequence and stop as soon
as a difference is found:
func isPalindrome(_ s: String) -> Bool {
return !zip(s.characters, s.characters.reversed()).contains { $0 != $1 }
}
s.characters.reversed() does not create a new collection in reverse
order, it just enumerates the characters from back to front.
With String(s.characters.reversed()) as in your method however,
you force the creation of a new collection for the reversed string,
that makes it slow.
For the 110-character string
let string = String(repeating: "Hello world", count: 10)
this reduces the computation time from about 6 sec to 1.2 sec in my test.
Next, avoid index calculations like
let endIndex = string.index(string.startIndex, offsetBy: length-1)
and iterate over the character index itself instead:
func partition(_ s: String) -> [[String]] {
var result: [[String]] = []
func dfs(string: String, partiton: [String]) {
if string.isEmpty {
result.append(partiton)
return
}
var idx = string.startIndex
repeat {
string.characters.formIndex(after: &idx)
let part = string.substring(to: idx)
if isPalindrome(part) {
let leftPart = string.substring(from: idx)
dfs(string: leftPart, partiton: partiton + [part])
}
} while idx != string.endIndex
}
func isPalindrome(_ s: String) -> Bool {
return !zip(s.characters, s.characters.reversed()).contains { $0 != $1 }
}
dfs(string: s, partiton: [])
return result
}
Computation time is now 0.7 sec.
The next step is to avoid string indexing totally, and work with
an array of characters, because array indexing is fast. Even better,
use array slices which are fast to create and reference the original
array elements:
func partition(_ s: String) -> [[String]] {
var result: [[String]] = []
func dfs(chars: ArraySlice<Character>, partiton: [String]) {
if chars.isEmpty {
result.append(partiton)
return
}
for length in 1...chars.count {
let part = chars.prefix(length)
if isPalindrome(part) {
let leftPart = chars.dropFirst(length)
dfs(chars: leftPart, partiton: partiton + [String(part)])
}
}
}
func isPalindrome(_ c: ArraySlice<Character>) -> Bool {
return !zip(c, c.reversed()).contains { $0 != $1 }
}
dfs(chars: ArraySlice(s.characters), partiton: [])
return result
}
Computation time is now 0.08 sec.
If your string contains only characters in the "basic multilingual plane" (i.e. <= U+FFFF) then you can work with UTF-16 code points instead:
func partition(_ s: String) -> [[String]] {
var result: [[String]] = []
func dfs(chars: ArraySlice<UInt16>, partiton: [String]) {
if chars.isEmpty {
result.append(partiton)
return
}
for length in 1...chars.count {
let part = chars.prefix(length)
if isPalindrome(part) {
let leftPart = chars.dropFirst(length)
part.withUnsafeBufferPointer {
dfs(chars: leftPart, partiton: partiton + [String(utf16CodeUnits: $0.baseAddress!, count: length)])
}
}
}
}
func isPalindrome(_ c: ArraySlice<UInt16>) -> Bool {
return !zip(c, c.reversed()).contains { $0 != $1 }
}
dfs(chars: ArraySlice(s.utf16), partiton: [])
return result
}
Computation time is now 0.04 sec for the 110 character test string.
So some tips which potentially can improve the performance when working with Swift strings are
Iterate over the characters/indices sequentially. Avoid "jumping"
to the n'th position.
If you need "random" access to all characters, convert the string
to an array first.
Working with the UTF-16 view of a string can be faster than working
with the characters view.
Of course it depends on the actual use-case. In this application,
we were able to reduce the computation time from 6 sec to 0.04 sec,
that is a factor of 150.