Skip to content

Commit

Permalink
Longest anagram solver
Browse files Browse the repository at this point in the history
  • Loading branch information
rm-hull authored and Richard Hull committed Mar 26, 2016
1 parent fae0b8b commit 92e9392
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 29 deletions.
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ To build and run a standalone jar:
$ lein ring uberjar
$ java -jar target/ars-magna-0.1.0-standalone.jar

In both instances, the webapp starts on http://localhost:3000
In both instances, the webapp starts on http://localhost:3000. See the curl
examples below for usage.

### Docker image

Expand All @@ -46,7 +47,7 @@ Using all the letters to find multi-word anagrams, from a Clojure REPL:
(let [dict (load-word-list :en-GB)
index (partition-by-word-length dict)]
(sort
(multi-word index "compute" 3 nil)))
(multi-word index "compute" 3)))
; ("come put" "compute" "cote ump" "cut mope" "cut poem"
; "cute mop" "met coup" "mote cup" "mute cop" "tome cup")
```
Expand All @@ -72,6 +73,56 @@ returns the same anagrams:
]
```

### Longest word anagrams

Find the longest single words from the given word, without necessarily using
all the letters - at the REPL:

```clojure
(use 'ars-magna.dict)
(use 'ars-magna.solver)

(let [dict (load-word-list :en-GB)
index (partition-by-letters dict)]
(sort-by
(juxt (comp - count) identity)
(longest index "compute" 4)))
; ("compute" "comet" "coupe" "tempo" "come"
; "cope" "cote" "coup" "cute" "mope" "mote"
; "mute" "poem" "poet" "pout" "temp" "tome")
```

_(the `(sort-by (juxt (comp - count) identity) ...)` returns the words
sorted by longest first, then alphabetically)_

or querying the web service for the word 'compute':

$ curl -s http://localhost:3000/longest/compute | jq .

returns the same anagrams:

```json
[
"compute",
"comet",
"coupe",
"tempo",
"come",
"cope",
"cote",
"coup",
"cute",
"mope",
"mote",
"mute",
"poem",
"poet",
"pout",
"temp",
"tome"
]
```

## References

* https://en.wikipedia.org/wiki/Letter_frequency#Relative_frequencies_of_letters_in_the_English_language
Expand Down
3 changes: 2 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
[ring "1.4.0"]
[hiccup "1.0.5"]
[ring-logger-timbre "0.7.5"]
[metrics-clojure-ring "2.6.1"]]
[metrics-clojure-ring "2.6.1"]
[org.clojure/math.combinatorics "0.1.1"]]
:scm {:url "git@github.com:rm-hull/ars-magna.git"}
:ring {
:handler ars-magna.handler/app }
Expand Down
13 changes: 9 additions & 4 deletions src/ars_magna/dict.clj
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@
;(map s/lower-case)
))

(defn partition-by-word-length [dict]
(defn- partition-words-by [aggregator]
(fn [dict]
(reduce
(fn [acc word]
(update acc (count word) conj word))
(sorted-map)
dict))
(update acc (aggregator word) conj word))
{}
dict)))

(def partition-by-word-length (partition-words-by count))

(def partition-by-letters (partition-words-by sort))

(defn words-of-size
([index n] (words-of-size index n 0))
Expand Down
30 changes: 25 additions & 5 deletions src/ars_magna/handler.clj
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,35 @@
s/lower-case
(s/replace #"\W" "")))

(defn min-size [req default]
(Integer/parseInt
(or
(get-in req [:params :min])
(str default))))

(defroutes app-routes
(let [dict (load-word-list :en-GB)
index (partition-by-word-length dict)]
(GET "/multi-word" [word :as req]
word-length-index (partition-by-word-length dict)
sorted-letter-index (partition-by-letters dict)]

(GET "/multi-word/:word" [word :as req]
(json-exception-handler
(to-json identity
(sort
(multi-word
word-length-index
(clean word)
(min-size req 3))))))

(GET "/longest/:word" [word :as req]
(json-exception-handler
(to-json identity
(let [min-size (Integer/parseInt (or (get-in req [:params :min]) "3"))]
(sort
(multi-word index (clean word) min-size nil))))))))
(sort-by
(juxt (comp - count) identity)
(longest
sorted-letter-index
(clean word)
(min-size req 4))))))))

(def app
(->
Expand Down
30 changes: 20 additions & 10 deletions src/ars_magna/solver.clj
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
(ns ars-magna.solver
(:require
[clojure.string :as s]
[clojure.math.combinatorics :as c]
[ars-magna.dict :refer :all]))

(defn multi-word [index word min-size prefix]
(if (empty? word)
(s/trim prefix)
(flatten
(for [w (find-in index word min-size :en-GB)]
(multi-word
index
(remaining-chars word w)
min-size
(str w " " prefix))))))
(defn multi-word
([index word min-size]
(multi-word index word min-size nil))

([index word min-size prefix]
(if (empty? word)
(s/trim prefix)
(flatten
(for [w (find-in index word min-size :en-GB)]
(multi-word
index
(remaining-chars word w)
min-size
(str w " " prefix)))))))

(defn longest [index word min-size]
(->>
(range min-size (inc (count word)))
(mapcat #(map sort (c/combinations word %)))
(mapcat index)))
18 changes: 13 additions & 5 deletions test/ars_magna/dict_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@

(def test-dict
["hello" "hat" "gloves" "time" "normally"
"at" "banana" "leaf" "lead" "and" "you"])
"at" "banana" "leaf" "lead" "and" "you"
"melon" "lemon"
])

(deftest check-load-word-list
(is (= 99171 (count (load-word-list :en-GB)))))

(deftest check-partitioning
(deftest check-word-length-partitioning
(let [index (partition-by-word-length test-dict)]
(is (nil? (index 0)))
(is (nil? (index 1)))
(is (= (index 2) ["at"]))
(is (= (index 3) ["you" "and" "hat"]))
(is (= (index 4) ["lead" "leaf" "time"]))
(is (= (index 5) ["hello"]))
(is (= (index 5) ["lemon" "melon" "hello"]))
(is (= (index 6) ["banana" "gloves"]))
(is (nil? (index 7)))
(is (= (index 8) ["normally"]))))

(deftest check-letter-partitioning
(let [index (partition-by-letters test-dict)]
(is (= (index (sort "lemon")) ["lemon" "melon"]))
(is (= (index (sort "hello")) ["hello"]))
(is (nil? (index (sort "person"))))))

(deftest check-words-of-size
(let [index (partition-by-word-length test-dict)]
(is (= ["normally" "banana" "gloves" "hello"] (words-of-size index 12 5)))
(is (= ["normally" "banana" "gloves" "hello" "lead"
(is (= ["normally" "banana" "gloves" "lemon" "melon" "hello"] (words-of-size index 12 5)))
(is (= ["normally" "banana" "gloves" "lemon" "melon" "hello" "lead"
"leaf" "time" "you" "and" "hat" "at"] (words-of-size index 12)))
(is (= ["lead" "leaf" "time" "you" "and" "hat" "at"] (words-of-size index 4)))
(is (= ["you" "and" "hat"] (words-of-size index 3 3)))
Expand Down
10 changes: 8 additions & 2 deletions test/ars_magna/solver_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
[ars-magna.dict :refer :all]
[ars-magna.solver :refer :all]))

(deftest check-solver
(deftest check-multi-word-solver
(let [dict (load-word-list :en-GB)
index (partition-by-word-length dict)]
(is (= (sort (multi-word index "compute" 3 nil))
(is (= (sort (multi-word index "compute" 3))
["come put" "compute" "cote ump" "cut mope" "cut poem"
"cute mop" "met coup" "mote cup" "mute cop" "tome cup"]))))

(deftest check-longest-solver
(let [dict (load-word-list :en-GB)
index (partition-by-letters dict)]
(is (= (longest index "compute" 5)
["comet" "coupe" "tempo" "compute"]))))

0 comments on commit 92e9392

Please sign in to comment.