2
\$\begingroup\$

I have been trying to learn the various aspects of the languages I know so far to do the Daily Challenges in reddit (/r/DailyProgrammer). The one which I am working on just now asks for an alphabetised count of the occurrences of letters in a string e.g: "Hello World" -> "d:1 e:1 h:1 l:3 o:2 r:1 w:1".

I have tried to write this seemingly simple challenge in Swift (I had already done it in C#, JavaScript and F#), but after longer than should have been the case, I have arrived at the following solution which seems a bit messy to me:

func countLetters(input: String) -> String  {

    // Convert to Lowercase
    var lower = input.lowercaseString

    // Filter out invalid characters ([a-z] only)
    var validCharacters = filter(lower) { $0 >= Character("a") && $0 <= Character("z") }

    // Create a dictionary to build the letter counts
    var letterCounts = Dictionary<Character, Int32>()
    // Go through the characters and add to the dictionary
    for i in validCharacters {
        if letterCounts[i] != nil { // If already exists, add one to the count
            letterCounts[i] = letterCounts[i]! + 1
        } else {
            letterCounts[i] = 1 // If doesn't exist, add to the dictionary with a count of 1
        }
    }

    // Unfortunately the Dictionary does not seem to be sorted / sortable, so we need to
    // create an Array of the stringified version to allow sorting
    var stringifiedCounts = Array<String>()

    // Convert each dictionary item to a 'a:1' type string
    for (k,v) in letterCounts {
        stringifiedCounts.append("\(k):\(v) ")
    }

    // Sort the list
    sort(&stringifiedCounts)

    // Concatenate the results
    var output = ""
    for i in stringifiedCounts {
        output += i
    }

    // Remove the last space
    return output.substringToIndex(advance(output.startIndex, countElements(output) - 1))
}

I was going to ask the question in my reply on Reddit, but I am quite late with it, so I doubt anyone will be looking.

For those who have been more regularly using Swift, is there a cleaner way to deal with Dictionaries so that I don't have to covert it to an array before sorting? What are some of the cleverer ways to get the same results?

\$\endgroup\$
1
  • 5
    \$\begingroup\$ Assuming this is working code and it's not broken, this seems like it's much better suited for Code Review \$\endgroup\$
    – nhgrif
    Nov 22, 2014 at 17:57

2 Answers 2

2
\$\begingroup\$

The following looks like it is much simpler and most of the way there:

func countLetters (string:String) -> String {
  var dict : [Character:Int] = [:]
  for letter in string {
    dict[letter] = 1 + (dict[letter] ?? 0)
  }

 //  dict -> array of tuples -> sort based on tuple .0
 var dictSorted = map(dict) { $0 }.sorted { $0.0 < $1.0 }

 // your string to return.
 return " ".join (dictSorted { (key,val) -> String in return "\(key):\(val)" })
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks, simple and concise. I was sure there must be a more straightforward way to do this. I just need to remember that just because a particular collection does not have a built in closure (map/filter/reduce), I can still use the separate closure methods. \$\endgroup\$
    – fabiossa
    Nov 23, 2014 at 9:56
2
\$\begingroup\$

We're really doing two different things here.

First, we're building a dictionary of character counts, and second, we're building a string representation of that dictionary. As such, this should be two separate functions. The first takes a string and returns a [Character:Int] dictionary, and the second function takes the [Character:Int] dictionary and returns a string formatted how we want.

When we separate the two distinct tasks, we allow our code to be more flexible. If we ever want something different out of the resulting character/count dictionary, we can easily do that without duplicating code.

Also, I think this seems really appropriate as a String extension rather than a free function.

extension String {
    func letterOccurrences() -> [Character:Int] {
        var dict: [Character:Int] = [:]
        for letter in self {
            dict[letter] = 1 + (dict[letter] ?? 0)
        }
        return dict
    }
}

For the function to format this as a printable string, I might like it to take an argument specifying how to sort it. And for that, I'd make an enum.

enum CharIntSort {
    case None
    case AlphabeticAscending
    case AlphabeticDescending
    case NumericAscending
    case NumericDescending
}

func charIntDictDescription(dict:[Character:Int], sort:CharIntSort = .None) -> String {
    var sortedDict: [(Character,Int)] = Array()
    switch sort {
    case .AlphabeticAscending:
        sortedDict = map(dict){$0}.sorted{$0.0 < $1.0}
    case .AlphabeticDescending:
        sortedDict = map(dict){$0}.sorted{$0.0 > $1.0}
    case .NumericAscending:
        sortedDict = map(dict){$0}.sorted{$0.1 < $1.1}
    case .NumericDescending:
        sortedDict = map(dict){$0}.sorted{$0.1 > $1.1}
    case .None:
        sortedDict = map(dict){$0}
    }

    return " ".join(sortedDict.map({(key,val) -> String in return "\(key):\(val)"}))
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for this, very clear on how to do allow for multiple sort options. \$\endgroup\$
    – fabiossa
    Nov 23, 2014 at 9:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.