From 92e93926083bc50877f652eb39146e5086b39b64 Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Sat, 26 Mar 2016 18:25:04 +0000 Subject: [PATCH] Longest anagram solver --- README.md | 55 ++++++++++++++++++++++++++++++++-- project.clj | 3 +- src/ars_magna/dict.clj | 13 +++++--- src/ars_magna/handler.clj | 30 +++++++++++++++---- src/ars_magna/solver.clj | 30 ++++++++++++------- test/ars_magna/dict_test.clj | 18 +++++++---- test/ars_magna/solver_test.clj | 10 +++++-- 7 files changed, 130 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 127dae0..3f56ec1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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") ``` @@ -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 diff --git a/project.clj b/project.clj index 90dd46a..afbd8e4 100644 --- a/project.clj +++ b/project.clj @@ -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 } diff --git a/src/ars_magna/dict.clj b/src/ars_magna/dict.clj index 3971e7b..3de2cf5 100644 --- a/src/ars_magna/dict.clj +++ b/src/ars_magna/dict.clj @@ -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)) diff --git a/src/ars_magna/handler.clj b/src/ars_magna/handler.clj index 13c0711..a045328 100644 --- a/src/ars_magna/handler.clj +++ b/src/ars_magna/handler.clj @@ -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 (-> diff --git a/src/ars_magna/solver.clj b/src/ars_magna/solver.clj index ecfa92a..6c591e6 100644 --- a/src/ars_magna/solver.clj +++ b/src/ars_magna/solver.clj @@ -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))) diff --git a/test/ars_magna/dict_test.clj b/test/ars_magna/dict_test.clj index 0da136d..31db014 100644 --- a/test/ars_magna/dict_test.clj +++ b/test/ars_magna/dict_test.clj @@ -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))) diff --git a/test/ars_magna/solver_test.clj b/test/ars_magna/solver_test.clj index 0b874b9..6d33f66 100644 --- a/test/ars_magna/solver_test.clj +++ b/test/ars_magna/solver_test.clj @@ -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"]))))