-2

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.

  • Better on https://codereview.stackexchange.com/ – matt Mar 15 '21 at 04:39
  • 2
    “The output should have 8 subsets” Really??? I count many more than that. What are your 8? – matt Mar 15 '21 at 04:42
  • You mean like "a", "a1", "a1b", "a1b2", "1", "1b", "1b2", "b", "b2", "2"? – Eric33187 Mar 15 '21 at 05:45
  • I was sort of dismissive of a recursive solution for substrings in my answer, demonstrating that was easy to do iteratively. However, that doesn't really answer the question, so I updated my answer to include a recursive solution for it. It is unclear exactly what you mean by "subset". – Chip Jarred Mar 15 '21 at 08:54
  • @matt sorry my mistake, I was solving another problem. it should be 4 outputs. – learningSwiftEveryday Mar 15 '21 at 18:11
  • I still don’t get it. What are your 4 outputs? Why can’t you just tell us? Is it a secret? – matt Mar 15 '21 at 18:27
  • @matt lol no secret, it should a1b2, A1B2, A1b2, A1B2 – learningSwiftEveryday Mar 15 '21 at 19:50
  • 1
    Well that would have been totally unguessable from anything you said in your question. Try to write clearer questions! What would it have cost you to say that you want the uppercase / lowercase variants, rather than using the mysterious "subsets" terminology? – matt Mar 15 '21 at 20:15
  • 1
    So the problem is to find the upper- and lowercase variations of the original string. OK so now I understand what you were trying to achieve in your code. It's just that it didn't match your description of the problem. – Chip Jarred Mar 15 '21 at 21:34
  • Updated my answer to reflect the clarification – Chip Jarred Mar 15 '21 at 21:55

2 Answers2

1

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.

Chip Jarred
  • 1,066
  • 2
  • 4
  • Hey Chip, this is brilliant, so there should be 4 outputs. Since there are 4 possible outcomes. a1b2, A1b2, a1B2, A1B2 – learningSwiftEveryday Mar 15 '21 at 18:15
  • The use of the term "subset" in the question is the problematic part. Now I I see what you were trying to do in the code. What you need are the all the possible case variations of the original string. – Chip Jarred Mar 15 '21 at 21:36
  • Your answer is awesome. What if the swift library functions aren't available, how would approach this problem ? – learningSwiftEveryday Mar 15 '21 at 22:01
  • Why wouldn't the Swift library be available? They're part of the language. – Chip Jarred Mar 15 '21 at 22:04
  • yes they are a part of the language, during an interview where access to these functions are non-existent. Wouldn't there be errors in the process of trying to write programmatically correct code ? – learningSwiftEveryday Mar 15 '21 at 22:06
  • Are you asking what to do if you are restricted to primitive operations? Obviously you'd need to have additional code to build the substring as an actual string to pass to the recursed call. Or use pointers (don't do that). But the Swift standard library should always be available. Unlike other languages, even types like Int, Double, and String are actually defined in the library, and not the core language. If you look at the library code online you see, for example, that Int is a `struct` containing a `builtin.int32` (or some similar spelling). – Chip Jarred Mar 15 '21 at 22:08
  • If you don't have access to the standard Swift Library, then you can't code in Swift, because you don't have `Int`, `Float`, `Double`, `String`, `Array`, `Dictionary`... basically you don't have anything to work with. All of that is in the library. Now, it's fair for the interview to say you can't use `Foundation` or `AppKit` or `UIKit`. – Chip Jarred Mar 15 '21 at 22:10
  • BTW - the code in my answer should work without any `import` at all. I think not having to import any other module satisfies any requirement not to use external library. – Chip Jarred Mar 15 '21 at 22:13
  • Also my earlier statement about `Int`'s implementation in the library suggests that it's 32-bits, and now I can't edit it. That's platform dependent, but it's generally actually 64-bit. Last I looked, some 32-bit ARM processers are still supported (for older iOS devices). `Int` would be 32-bit for them, and 64-bit pretty much everywhere else. It's not relevant for the question at hand, but I didn't want that to mislead anyone. – Chip Jarred Mar 15 '21 at 22:22
  • thank you for responding, I appreciate it. I don't think I used the right word. I meant what if they allowed you use .map and .fitler but not .startIndex or .endIndex. Like imagine trying to solve it like how I tried to solve it. – learningSwiftEveryday Mar 15 '21 at 22:24
  • 1
    It's more likely they'd disallow `.map` and `.filter` than `.startIndex` and `.endIndex`, the latter pair being more primitive in some sense. However, all of those are defined in the standard protocols that `String` conforms to like `Collection`, so they should all be fair game, and those protocols are part of the same standard library that defines `String` itself. If `.map` were disallowed, you'd iterate over results and build each string yourself. If they disallow `.startIndex`... walk out. You don't want to work for such unreasonable people. – Chip Jarred Mar 15 '21 at 22:30
  • Thanks for the response man. You're the GOAT !!!!!!! – learningSwiftEveryday Mar 15 '21 at 22:53
  • I was a little harsh with my statement about walking out if `.startIndex` were disallowed. There is a solution using `.dropFirst()`. It's just that `.startIndex` is kind of a fundamental part of the API for getting individual `Character`s from the string that I think disallowing it is completely mental. – Chip Jarred Mar 16 '21 at 00:01
  • 1
    Updated with a discussion about using `.dropFirst()` – Chip Jarred Mar 16 '21 at 00:24
  • The lecture about String and characters is as wrong as can be. – matt Mar 16 '21 at 06:05
  • @matt, do you mean about `String.count`? I just checked Apple documentation, and it does say that it is the number of characters, though I have a firm memory of reading otherwise. Either it's changed since I read it (which was some time ago), or I'm just going nuts and misremembering. Or were you referring to something else? – Chip Jarred Mar 16 '21 at 09:01
  • @matt I just edited my answer to correct that error. Thank you for pointing it out... I could have sworn I had encountered an official statement to the contrary, but if I did, it certainly isn't the case now. – Chip Jarred Mar 16 '21 at 09:11
1

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)
matt
  • 447,615
  • 74
  • 748
  • 977