3
\$\begingroup\$

"Using names.txt (right click and 'Save Link/Target As...'), a 46K text file containing over five-thousand first names, begin by sorting it into alphabetical order. Then working out the alphabetical value for each name, multiply this value by its alphabetical position in the list to obtain a name score.

For example, when the list is sorted into alphabetical order, COLIN, which is worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would obtain a score of 938 × 53 = 49714.

What is the total of all the name scores in the file?"

I'm relatively new to clojure, and this is what I came up with:

(def names (sort (map (fn[x] (replace x #"\"" "")) (split (slurp "/users/calvinfroedge/Downloads/names.txt") #","))))

(loop [i 0 total 0]
  (if (not= i (count names))
    (recur (inc i) (+ total (* (inc i) (reduce + (map (fn[x] (- (int x) 64)) (nth names i))))))
    total)

I read somewhere that doseq is preferred to loop/recur, but it wasn't apparent to me how to comprehensibly AND idiomatically approach this problem without using an explicit loop with an incrementing value.

Am I missing something?

\$\endgroup\$
1
  • \$\begingroup\$ doseq is not preferable to loop ... recur. It requires mutated state to do anything, as it returns nil. Where both apply, use the latter. Was what you read, perhaps, that lazy sequences and the sequence library were preferable to loop ... recur? \$\endgroup\$
    – Thumbnail
    Commented Jun 1, 2014 at 9:21

3 Answers 3

7
\$\begingroup\$

Yes. I think you still miss a very useful part of clojure - using its various collection types to model your solution. In your problem, you could use the fact that a string is a seq and a map acts as a function to calculate the cost of a word:

;; this map will act as a function from a name to its ordinal
(def name->position (zipmap names (map inc (range))))

;; and this map will act as a function from letter to its ordinal
(def ab-map (zipmap alphabet (map inc (range))))

Now that we have these two functions, we could easily calculate a name's value in this way:

(defn score [word]
  (* (reduce + (map ab-map word))
     (name->position word 0)))

You can now calculate COLIN: (score "COLIN")

Explicit recursion can many times be avoided by using functions like map and reduce, mainly because often the sequences themselves are defined in terms of recursion

From here, you could solve this question by using map and reduce with the new function score

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your answer. I updated my question as I had made some improvements already. \$\endgroup\$ Commented Apr 29, 2014 at 23:34
  • 1
    \$\begingroup\$ zipmap is really cool! \$\endgroup\$ Commented Apr 29, 2014 at 23:35
3
\$\begingroup\$

I would consider using map-indexed as a way to use the name's "position" (i.e. index) as part of the scoring function.

As @Shlomi mentioned, you can often avoid using recursion/loops by taking a more functional approach and use something like map or reduce to transform your collection of data into your desired result. I would approach a problem like this by defining your data set, writing a series of helper functions, and then bringing it all together at the end:

(require '[clojure.string :as s :only (replace split)])

(def sorted-names
  (-> 
    (slurp "/users/calvinfroedge/Downloads/names.txt")
    (s/replace #"\"" "")
    (s/split #",")
    sort))

(def letter->value
  (zipmap "ABCDEFGHIJKLMNOPQRSTUVWXYZ" (map inc (range))))

(def scored-names
  (map-indexed (fn [i name] 
                 (* (inc i) (reduce + (map letter->value name))))
               sorted-names))

(def total-score
  (reduce + scored-names))

I really like the idea of using zipmap to easily create a function that gives you the value of a letter, so I've stolen it. :)

This is nitpicky, but I've also simplified your "name collection" algorithm by moving the (replace #"\"" "") step before the (split #",") step (since you're starting with one big string, you can just get rid of all of the "s all at once, rather than doing it individually for each name after you split them up), and then putting the whole thing into a threading macro (->) for better readability.

\$\endgroup\$
2
\$\begingroup\$

We can succinctly build the required expression from the bottom.

(let [dec-int-A (->> \A int dec)
      letter-score #(- (int %) dec-int-A)
      word-score #(->> % (map letter-score) (apply +))
      dict-score #(->> % sort (map word-score) (map * (iterate inc 1)) (apply +))]
  (dict-score (read-string (str \[ (slurp "names.txt") \]))))

There are some novelties:

  • using ->> to flatten expressions;
  • using (iterate inc 1) to tag the word scores with the correct positions;
  • using read-string directly on the square-bracketed text (the edn version would be safer).

The effect is, I hope, to make the solution clear and easy to read.

\$\endgroup\$
1
  • \$\begingroup\$ Beautifully concise. \$\endgroup\$ Commented Jun 4, 2014 at 18:04

Your Answer

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

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