Teacup rim text: Difference between revisions
→Haskell :: Filtering anagrams: Pruned out redundant rotations function. |
Thundergnat (talk | contribs) →{{header|Perl 6}}: Update to meet the ever-shifting task requirements |
||
Line 601: | Line 601: | ||
for %words.values -> @these { |
for %words.values -> @these { |
||
next if @these < 2; |
|||
MAYBE: for @these { |
MAYBE: for @these { |
||
my $maybe = $_; |
my $maybe = $_; |
||
Line 611: | Line 612: | ||
} else { |
} else { |
||
@print = (); |
@print = (); |
||
next MAYBE |
|||
} |
} |
||
} |
} |
||
Line 623: | Line 624: | ||
say .join(", ") for sort @teacups;</lang> |
say .join(", ") for sort @teacups;</lang> |
||
{{out}} |
{{out}} |
||
<pre> |
<pre>AIM, IMA, MAI |
||
AIM, IMA, MAI |
|||
ARC, RCA, CAR |
ARC, RCA, CAR |
||
ASP, SPA, PAS |
ASP, SPA, PAS |
||
ATE, TEA, EAT |
ATE, TEA, EAT |
||
IPS, PSI, SIP</pre> |
|||
IPS, PSI, SIP |
|||
OOO, OOO, OOO |
|||
WWW, WWW, WWW |
|||
XXX, XXX, XXX</pre> |
|||
=={{header|Python}}== |
=={{header|Python}}== |
Revision as of 10:03, 6 August 2019
On a set of coasters we have, there's a picture of a teacup. On the rim of the teacup the word "TEA" appears a number of times separated by bullet characters. It occurred to me that if the bullet were removed and the words run together, you could start at any letter and still end up with a meaningful three-letter word. So start at the "T" and read "TEA". Start at the "E" and read "EAT", or start at the "A" and read "ATE".
That got me thinking that maybe there are other words that could be used rather that "TEA". And that's just English. What about Italian or Greek or ... um ... Telugu. For English, use the MIT 10000 word list located at https://www.mit.edu/~ecprice/wordlist.10000
So here's the task: You're in search of a set of words that could be printed around the edge of a teacup. The words in each set are to be of the same length, that length being greater than two (thus precluding AH and HA, for example.) The words should also be made of more than one letter (thus precluding III and OOO etc.)
The relationship between these words is (using ATE as an example) that the first letter of the first becomes the last letter of the second. The first letter of the second becomes the last letter of the third. So ATE becomes TEA and TEA becomes EAT. All of the possible permutations, using this particular permutation technique, must be words in the list. The set you generate for ATE will never included the word ETA as that cannot be reached via the first-to-last movement method.
Display one line for each set of teacup rim words.
Factor
<lang factor>USING: combinators.short-circuit fry grouping hash-sets http.client kernel math prettyprint sequences sequences.extras sets sorting splitting ;
"https://www.mit.edu/~ecprice/wordlist.10000" http-get nip "\n" split [ { [ length 3 < ] [ all-equal? ] } 1|| ] reject [ [ all-rotations ] map ] [ >hash-set ] bi '[ [ _ in? ] all? ] filter [ natural-sort ] map members .</lang>
- Output:
{ { "aim" "ima" "mai" } { "arc" "car" "rca" } { "asp" "pas" "spa" } { "ate" "eat" "tea" } { "ips" "psi" "sip" } }
Go
<lang go>package main
import (
"bufio" "fmt" "log" "os" "sort" "strings"
)
func check(err error) {
if err != nil { log.Fatal(err) }
}
func readWords(fileName string) []string {
file, err := os.Open(fileName) check(err) defer file.Close() var words []string scanner := bufio.NewScanner(file) for scanner.Scan() { word := strings.ToLower(strings.TrimSpace(scanner.Text())) if len(word) >= 3 { words = append(words, word) } } check(scanner.Err()) return words
}
func rotate(runes []rune) {
first := runes[0] copy(runes, runes[1:]) runes[len(runes)-1] = first
}
func main() {
words := readWords("mit_10000.txt") // using local copy n := len(words) used := make(map[string]bool)
outer:
for _, word := range words { runes := []rune(word) variants := []string{word} for i := 0; i < len(runes)-1; i++ { rotate(runes) word2 := string(runes) if word == word2 || used[word2] { continue outer } ix := sort.SearchStrings(words, word2) if ix == n || words[ix] != word2 { continue outer } variants = append(variants, word2) } for _, variant := range variants { used[variant] = true } fmt.Println(variants) }
}</lang>
- Output:
[aim ima mai] [arc rca car] [asp spa pas] [ate tea eat] [ips psi sip]
Haskell
Using Data.Set
Circular words of more than 2 characters in a local copy of a word list. <lang haskell>import Data.List (groupBy, intercalate, sort, sortBy) import qualified Data.Set as S import Data.Ord (comparing) import Data.Function (on)
main :: IO ()
main =
readFile "mitWords.txt" >>= (putStrLn . showGroups . circularWords . lines)
circularWords :: [String] -> [String] circularWords ws =
let lexicon = S.fromList ws in filter (isCircular lexicon) ws
isCircular :: S.Set String -> String -> Bool isCircular lex w = 2 < length w && all (`S.member` lex) (rotations w)
rotations :: [a] -> a rotations = fmap <$> rotated <*> (enumFromTo 0 . pred . length)
rotated :: [a] -> Int -> [a] rotated [] _ = [] rotated xs n = zipWith const (drop n (cycle xs)) xs
showGroups :: [String] -> String showGroups xs =
unlines $ intercalate " -> " . fmap snd <$> groupBy (on (==) fst) (sortBy (comparing fst) (((,) =<< sort) <$> xs))</lang>
- Output:
aaa arc -> car -> rca ate -> eat -> tea aim -> ima -> mai asp -> pas -> spa iii ips -> psi -> sip ooo www xxx
Filtering anagrams
Or taking a different approach, we can avoid the use of Data.Set by obtaining the groups of anagrams (of more than two characters) in the lexicon, and filtering out a circular subset of these: <lang haskell>import Data.List (groupBy, intercalate, sort, sortBy) import Data.Ord (comparing) import Data.Function (on) import Data.Bool (bool)
main :: IO () main =
readFile "mitWords.txt" >>= (putStrLn . unlines . fmap (intercalate " -> ") . (circularOnly =<<) . anagrams . lines)
anagrams :: [String] -> String anagrams ws =
groupBy (on (==) fst) (sortBy (comparing fst) (((,) =<< sort) <$> ws)) >>= (bool [] . return . fmap snd) <*> ((> 2) . length)
circularOnly :: [String] -> String circularOnly ws =
let h = head ws lng = length h - 1 rs = filter (isRotation h) (tail ws) in bool [h : rs] [] (lng > length rs)
isRotation :: String -> String -> Bool isRotation xs ys = xs /= until ((||) . (ys ==) <*> (xs ==)) rotated (rotated xs)
rotated :: [a] -> [a] rotated [] = [] rotated xs = tail xs ++ [head xs]</lang>
- Output:
arc -> rca -> car ate -> tea -> eat aim -> ima -> mai asp -> spa -> pas ips -> psi -> sip
JavaScript
Set() objects
Reading a local dictionary with the macOS JS for Automation library:
<lang javascript>(() => {
'use strict';
// main :: IO () const main = () => showGroups( circularWords( // Local copy of: // https://www.mit.edu/~ecprice/wordlist.10000 lines(readFile('~/mitWords.txt')) ) );
// circularWords :: [String] -> [String] const circularWords = ws => ws.filter(isCircular(new Set(ws)), ws);
// isCircular :: Set String -> String -> Bool const isCircular = lexicon => w => { const iLast = w.length - 1; return 1 < iLast && until( ([i, bln, s]) => iLast < i || !bln, ([i, bln, s]) => [1 + i, lexicon.has(s), rotated(s)], [0, true, w] )[1]; }
// DISPLAY --------------------------------------------
// showGroups :: [String] -> String const showGroups = xs => unlines(map( gp => map(snd, gp).join(' -> '), groupBy( (a, b) => fst(a) === fst(b), sortBy( comparing(fst), map(x => Tuple(concat(sort(chars(x))), x), xs ) ) ) ));
// MAC OS JS FOR AUTOMATION ---------------------------
// readFile :: FilePath -> IO String const readFile = fp => { const e = $(), uw = ObjC.unwrap, s = uw( $.NSString.stringWithContentsOfFileEncodingError( $(fp) .stringByStandardizingPath, $.NSUTF8StringEncoding, e ) ); return undefined !== s ? ( s ) : uw(e.localizedDescription); };
// GENERIC FUNCTIONS ----------------------------------
// Tuple (,) :: a -> b -> (a, b) const Tuple = (a, b) => ({ type: 'Tuple', '0': a, '1': b, length: 2 });
// chars :: String -> [Char] const chars = s => s.split();
// comparing :: (a -> b) -> (a -> a -> Ordering) const comparing = f => (x, y) => { const a = f(x), b = f(y); return a < b ? -1 : (a > b ? 1 : 0); };
// concat :: a -> [a] // concat :: [String] -> String const concat = xs => 0 < xs.length ? (() => { const unit = 'string' !== typeof xs[0] ? ( [] ) : ; return unit.concat.apply(unit, xs); })() : [];
// fst :: (a, b) -> a const fst = tpl => tpl[0];
// groupBy :: (a -> a -> Bool) -> [a] -> a const groupBy = (f, xs) => { const tpl = xs.slice(1) .reduce((a, x) => { const h = a[1].length > 0 ? a[1][0] : undefined; return (undefined !== h) && f(h, x) ? ( Tuple(a[0], a[1].concat([x])) ) : Tuple(a[0].concat([a[1]]), [x]); }, Tuple([], 0 < xs.length ? [xs[0]] : [])); return tpl[0].concat([tpl[1]]); };
// lines :: String -> [String] const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b] const map = (f, xs) => (Array.isArray(xs) ? ( xs ) : xs.split()).map(f);
// rotated :: String -> String const rotated = xs => xs.slice(1) + xs[0];
// showLog :: a -> IO () const showLog = (...args) => console.log( args .map(JSON.stringify) .join(' -> ') );
// snd :: (a, b) -> b const snd = tpl => tpl[1];
// sort :: Ord a => [a] -> [a] const sort = xs => xs.slice() .sort((a, b) => a < b ? -1 : (a > b ? 1 : 0));
// sortBy :: (a -> a -> Ordering) -> [a] -> [a] const sortBy = (f, xs) => xs.slice() .sort(f);
// unlines :: [String] -> String const unlines = xs => xs.join('\n');
// until :: (a -> Bool) -> (a -> a) -> a -> a const until = (p, f, x) => { let v = x; while (!p(v)) v = f(v); return v; };
// MAIN --- return main();
})();</lang>
- Output:
aaa arc -> car -> rca ate -> eat -> tea aim -> ima -> mai asp -> pas -> spa iii ips -> psi -> sip ooo www xxx
Anagram filtering
Reading a local dictionary with the macOS JS for Automation library:
<lang javascript>(() => {
'use strict';
// main :: IO () const main = () => anagrams(lines(readFile('~/mitWords.txt'))) .flatMap(circularOnly) .map(xs => xs.join(' -> ')) .join('\n')
// anagrams :: [String] -> String const anagrams = ws => groupBy( on(eq, fst), sortBy( comparing(fst), ws.map(w => Tuple(sort(chars(w)).join(), w)) ) ).flatMap( gp => 2 < gp.length ? [ gp.map(snd) ] : [] )
// circularOnly :: [String] -> String const circularOnly = ws => { const h = ws[0]; return ws.length < h.length ? ( [] ) : (() => { const rs = rotations(h); return rs.every(r => ws.includes(r)) ? ( [rs] ) : []; })(); };
// rotations :: String -> [String] const rotations = s => takeIterate(s.length, rotated, s)
// rotated :: [a] -> [a] const rotated = xs => xs.slice(1).concat(xs[0]);
// GENERIC FUNCTIONS ----------------------------
// Tuple (,) :: a -> b -> (a, b) const Tuple = (a, b) => ({ type: 'Tuple', '0': a, '1': b, length: 2 });
// chars :: String -> [Char] const chars = s => s.split();
// comparing :: (a -> b) -> (a -> a -> Ordering) const comparing = f => (x, y) => { const a = f(x), b = f(y); return a < b ? -1 : (a > b ? 1 : 0); };
// eq (==) :: Eq a => a -> a -> Bool const eq = (a, b) => a === b
// fst :: (a, b) -> a const fst = tpl => tpl[0];
// groupBy :: (a -> a -> Bool) -> [a] -> a const groupBy = (f, xs) => { const tpl = xs.slice(1) .reduce((a, x) => { const h = a[1].length > 0 ? a[1][0] : undefined; return (undefined !== h) && f(h, x) ? ( Tuple(a[0], a[1].concat([x])) ) : Tuple(a[0].concat([a[1]]), [x]); }, Tuple([], 0 < xs.length ? [xs[0]] : [])); return tpl[0].concat([tpl[1]]); };
// lines :: String -> [String] const lines = s => s.split(/[\r\n]/);
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y]) const mapAccumL = (f, acc, xs) => xs.reduce((a, x, i) => { const pair = f(a[0], x, i); return Tuple(pair[0], a[1].concat(pair[1])); }, Tuple(acc, []));
// on :: (b -> b -> c) -> (a -> b) -> a -> a -> c const on = (f, g) => (a, b) => f(g(a), g(b));
// readFile :: FilePath -> IO String const readFile = fp => { const e = $(), uw = ObjC.unwrap, s = uw( $.NSString.stringWithContentsOfFileEncodingError( $(fp) .stringByStandardizingPath, $.NSUTF8StringEncoding, e ) ); return undefined !== s ? ( s ) : uw(e.localizedDescription); };
// snd :: (a, b) -> b const snd = tpl => tpl[1];
// sort :: Ord a => [a] -> [a] const sort = xs => xs.slice() .sort((a, b) => a < b ? -1 : (a > b ? 1 : 0));
// sortBy :: (a -> a -> Ordering) -> [a] -> [a] const sortBy = (f, xs) => xs.slice() .sort(f);
// takeIterate :: Int -> (a -> a) -> a -> [a] const takeIterate = (n, f, x) => snd(mapAccumL((a, _, i) => { const v = 0 !== i ? f(a) : x; return [v, v]; }, x, Array.from({ length: n })));
// MAIN --- return main();
})();</lang>
- Output:
arc -> rca -> car ate -> tea -> eat aim -> ima -> mai asp -> spa -> pas ips -> psi -> sip
Julia
Using the MIT 10000 word list, and excluding words of less than three letters, to reduce output length. <lang julia>using HTTP
rotate(s, n) = String(circshift(Vector{UInt8}(s), n))
isliketea(w, d) = (n = length(w); n > 2 && all(i -> haskey(d, rotate(w, i)), 1:n-1))
function getteawords(listuri)
req = HTTP.request("GET", listuri) wdict = Dict{String, Int}((string(x), 1) for x in split(String(req.body), r"\s+")) sort(unique([sort([rotate(word, i) for i in 1:length(word)]) for word in collect(keys(wdict)) if isliketea(word, wdict)]))
end
foreach(println, getteawords("https://www.mit.edu/~ecprice/wordlist.10000"))
</lang>
- Output:
["aaa", "aaa", "aaa"] ["aim", "ima", "mai"] ["arc", "car", "rca"] ["asp", "pas", "spa"] ["ate", "eat", "tea"] ["iii", "iii", "iii"] ["ips", "psi", "sip"] ["ooo", "ooo", "ooo"] ["www", "www", "www"] ["xxx", "xxx", "xxx"]
Lychen
Lychen is V8 JavaScript wrapped in C#, exposing C# into JavaScript.
Using https://www.mit.edu/~ecprice/wordlist.10000 as per the Julia example.
<lang javascript> const wc = new CS.System.Net.WebClient(); const lines = wc.DownloadString("https://www.mit.edu/~ecprice/wordlist.10000"); const words = lines.split(/\n/g); const collection = {}; words.filter(word => word.length > 2).forEach(word => {
let allok = true; let newword = word; for (let i = 0; i < word.length - 1; i++) { newword = newword.substr(1) + newword.substr(0, 1); if (!words.includes(newword)) { allok = false; break; } } if (allok) { const key = word.split("").sort().join(""); if (!collection[key]) { collection[key] = [word]; } else { if (!collection[key].includes(word)) { collection[key].push(word); } } }
}); Object.keys(collection) .filter(key => collection[key].length > 1) .forEach(key => console.log("%s", collection[key].join(", "))); </lang>
aim, ima, mai arc, car, rca asp, pas, spa ate, eat, tea ips, psi, sip
Perl 6
Much of the previous exposition here is superfluous as the reference word list has changed.
There doesn't seem to be any restriction that the word needs to consist only of lowercase letters, so words of any case are included. Since the example code specifically shows the example words (TEA, EAT, ATE) in uppercase, I elected to uppercase the found words.
<lang perl6>my %words; './wordlist.10000'.IO.slurp.words.map: { .chars < 3 ?? (next) !! %words{.uc.comb.sort.join}.push: .uc };
my @teacups; my %seen;
for %words.values -> @these {
next if @these < 2; MAYBE: for @these { my $maybe = $_; next if %seen{$_}; my @print; for ^$maybe.chars { if $maybe ∈ @these { @print.push: $maybe; $maybe = $maybe.comb.list.rotate.join; } else { @print = (); next MAYBE } } if @print.elems { @teacups.push: @print; %seen{$_}++ for @print; } }
}
say .join(", ") for sort @teacups;</lang>
- Output:
AIM, IMA, MAI ARC, RCA, CAR ASP, SPA, PAS ATE, TEA, EAT IPS, PSI, SIP
Python
Functional
Composing generic functions, and taking only circular words of more than two characters. <lang python>Teacup rim text
from itertools import groupby from os.path import expanduser
- main :: IO ()
def main():
Circular words of more than two characters in a local copy of a word list. print( showGroups( circularWords( # Local copy of # https://www.mit.edu/~ecprice/wordlist.10000 lines(readFile('~/mitWords.txt')) ) ) )
- circularWords :: [String] -> [String]
def circularWords(ws):
The subset of those words in the given list which are circular. lexicon = set(ws) return list(filter(isCircular(lexicon), ws))
- isCircular :: Set String -> String -> Bool
def isCircular(lexicon):
True if the given word contains more than two characters, and all of its rotations are found in the lexicon. def go(w): def f(tpl): (i, _, x) = tpl return (1 + i, x in lexicon, rotated(x))
iLast = len(w) - 1 return 1 < iLast and until( lambda tpl: iLast < tpl[0] or (not tpl[1]) )(f)( (0, True, w) )[1]
return lambda s: go(s)
- DISPLAY -------------------------------------------------
- showGroups :: [String] -> String
def showGroups(xs):
List of groups of circular words. return '\n'.join([ ' -> '.join([snd(g) for g in gp]) for gp in groupBy(fst)( sorted( [(.join(sorted(x)), x) for x in xs], key=fst ) ) ])
- GENERIC -------------------------------------------------
- fst :: (a, b) -> a
def fst(tpl):
First member of a pair. return tpl[0]
- groupBy :: (a -> b) -> [a] -> a
def groupBy(f):
The elements of xs grouped, preserving order, by equality in terms of the key function f. return lambda xs: [ list(x[1]) for x in groupby(xs, key=f) ]
- lines :: String -> [String]
def lines(s):
A list of strings, (containing no newline characters) derived from a single new-line delimited string. return s.splitlines()
- readFile :: FilePath -> IO String
def readFile(fp):
The contents of any file at the path derived by expanding any ~ in fp. with open(expanduser(fp), 'r', encoding='utf-8') as f: return f.read()
- rotated :: String -> String
def rotated(s):
A list rotated 1 character to the right. return s[1:] + s[0]
- snd :: (a, b) -> b
def snd(tpl):
Second member of a pair. return tpl[1]
- until :: (a -> Bool) -> (a -> a) -> a -> a
def until(p):
The result of repeatedly applying f until p holds. The initial seed value is x. def go(f, x): v = x while not p(v): v = f(v) return v return lambda f: lambda x: go(f, x)
- MAIN ---
if __name__ == '__main__':
main()</lang>
- Output:
aaa arc -> car -> rca ate -> eat -> tea aim -> ima -> mai asp -> pas -> spa iii ips -> psi -> sip ooo www xxx
REXX
All words that contained non─letter (Latin) characters (periods, decimal digits, minus signs, underbars, or embedded blanks) weren't considered as candidates for circular words.
Duplicated words (such as sop and SOP) are ignored (just the 2nd and subsequent words).
The dictionary wasn't assumed to be sorted in any way. <lang rexx>/*REXX pgm finds circular words (length>2), using a dictionary, suppress permutations.*/ parse arg iFID L . /*obtain optional arguments from the CL*/ if iFID==|iFID=="," then iFID= 'wordlist.10k' /*Not specified? Then use the default.*/ if L==| L=="," then L= 3 /* " " " " " " */
- = 0 /*number of words in dictionary, Len>L.*/
@.= /*stemmed array of non─duplicated words*/
do r=0 while lines(iFID)\==0 /*read all lines (words) in dictionary.*/ z= space( linein(iFID) ) /*obtain get a word from the dictionary*/ if length(z)<L | @.z\== then iterate /*length must be L or more, no dups.*/ if \datatype(z, 'M') then iterate /*Word contains non-letters? Then skip*/ @.z = z /*assign a word from the dictionary. */ #= # + 1; $.#= z /*bump word count; append word to list.*/ end /*r*/ /* [↑] dictionary need not be sorted. */
cw= 0 /*the number of circular words (so far)*/ say "There're " r ' entries in the dictionary (of all types): ' iFID say "There're " # ' words in the dictionary of at least length ' L say
do j=1 for #; x= $.j; y= x /*obtain the Jth word in the list. */ if x== then iterate /*if a null, don't show variants. */ yy= y /*the start of a list of the variants. */ do k=1 for length(x)-1 /*"circulate" the litters in the word. */ y= substr(y, 2)left(y, 1) /*add the left letter to the right end.*/ if @.y== then iterate j /*if not a word, then skip this word. */ yy= yy',' y /*append to the list of the variants. */ if y\==x then @.y= /*nullify word to suppress permutations*/ end /*k*/ /* [↓] ··· except for monolithic words.*/ cw= cw + 1 /*bump counter of circular words found.*/ say 'circular word: ' yy /*display a circular word and variants.*/ end /*j*/
say say cw ' circular words were found.' /*stick a fork in it, we're all done. */</lang>
- output when using the default inputs:
There're 10000 entries in the dictionary (of all types): wordlist.10k There're 9578 words in the dictionary of at least length 3 circular word: aaa, aaa, aaa circular word: aim, ima, mai circular word: arc, rca, car circular word: asp, spa, pas circular word: ate, tea, eat circular word: iii, iii, iii circular word: ips, psi, sip circular word: ooo, ooo, ooo circular word: www, www, www circular word: xxx, xxx, xxx 10 circular words were found.
zkl
<lang zkl>// this is limited to the max items a Dictionary can hold words:=File("mit_wordlist_10000.txt").pump(Dictionary().add.fp1(True),"strip"); seen :=Dictionary(); foreach word in (words.keys){
rots,w,sz := List(), word, word.len(); if(sz>2 and not seen.holds(word)){ do(sz-1){
w=String(w[-1],w[0,-1]); // rotate one character if(not words.holds(w)) continue(2); // not a word, do next word rots.append(w); // I'd like to see all the rotations
} println(rots.append(word).sort().concat(" ")); rots.pump(seen.add.fp1(True)); // we've seen these rotations }
}</lang>
- Output:
www www www asp pas spa ips psi sip iii iii iii ate eat tea aaa aaa aaa aim ima mai arc car rca xxx xxx xxx ooo ooo ooo