# Find the frequency of triplets in a phrase

For context, this problem is based on a old chat-bot project I did.

# Problem:

Given a string of words containing any of the characters:

¶
D
¶+
:$.&¶  Try it online! ### Explanation TlLd plld _  Replace all uppercase letters with lowercase ones, keep other letters, digits and spaces unchanged and delete other characters. M!&(\b\w+ ??){3}  Find all possible matches of three words in a row. O  Sort the matches, so equal triplets end next to each other. $
¶


Add a newline at the end.

D


Remove duplicate lines (this will keep the newlines at the end of them).

¶+
:$.&¶  Sequences of newlines are now the counts we need, replace them with a delimiter (:) followed by the count and a newline. • Tw p_dll _ saves a byte. – Martin Ender Mar 7 '17 at 14:09 # Mathematica, 75 bytes Tally@Partition[ToLowerCase@#~StringSplit~RegularExpression@"[_\\W]+",3,1]&  Pure function taking a string as input and returning a list of ordered triples of strings together with their number of appearances. For example, on the input "aa bb a bb a bb a cc a bb a" the output is {{{"aa", "bb", "a"}, 1}, {{"bb", "a", "bb"}, 2}, {{"a", "bb", "a"}, 3}, {{"bb", "a", "cc"}, 1}, {{"a", "cc", "a"}, 1}, {{"cc", "a", "bb"}, 1}}. The expression RegularExpression@"[_\\W]+" recognizes all runs of one or more non-word characters (that's what \\W does, except it calls _ a word character whereas we don't want to). ToLowerCase@#~StringSplit~ then splits the input string at all of those runs, after converting all letters to lowercase. Partition[...,3,1] partitions the resulting list into all triples of consecutive words, and Tally does exactly what we want to that list of triples. • The string shouldn't be split around non-word characters but only around spaces. Other non-word characters should simply be deleted. See e.g. the test case containing What's. – Martin Ender Mar 5 '17 at 11:06 • StringSplit splits around spaces by default. – LegionMammal978 Mar 5 '17 at 16:16 # Java 8, 376 372 bytes import java.util.*;public interface T{static void main(String[]k)throws Exception{String s="";int c;while((c=System.in.read())>10)if(c==32|(""+(char)c).matches("[A-Za-z]"))s+=(char)c;k=s.toLowerCase().split(" ");Map<String,Integer>o=new HashMap<>();for(int i=0;i<k.length-2;i++){s=k[i]+" "+k[i+1]+" "+k[i+2];o.put(s,(o.containsKey(s)?o.get(s):0)+1);}System.out.print(o);}}  Ungolfed: import java.util.HashMap; import java.util.Map; public interface T { static void main(String[] k) throws Exception { String s = ""; int c; while ((c = System.in.read()) > 10) if (c == 32 | ("" + (char) c).matches("[A-Za-z]")) s += (char) c; k = s.toLowerCase().split(" "); Map<String, Integer> o = new HashMap<>(); for (int i = 0; i < k.length - 2; i++) { s = k[i] + " " + k[i + 1] + " " + k[i + 2]; o.put(s, (o.containsKey(s) ? o.get(s) : 0) + 1); } System.out.print(o); } }  Never use Java for actual golfing. EDIT: Saved 4 bytes by abusing the fact that the main method has a String[] as its parameter so I don't have to initialize one >:-) • I'm getting 376 bytes still. I think you forgot to update your code. (String[]a->String[]k and delete ,k[]) – CAD97 Mar 5 '17 at 3:52 • @CAD97 oh whoops, you're right. That was stupid. Thanks! – HyperNeutrino Mar 5 '17 at 3:53 • Submitting this as a lambda Function<String, String> that takes s as input and returns o.toString() should be shorter by a lot of bytes. So import java.util.*; & s->{String[]k=s.toLowerCase().split(" "); etc. – CAD97 Mar 5 '17 at 4:03 # 05AB1E, 23 20 bytes žLðìÃ#Œ3ù©Ùvyðý®y¢‚ˆ  Try it online! Explanation žL # push a string of alphanumeric characters ðì # prepend a space Ã # keep only those characters from input # # split on spaces Œ3ù # get all sublists of length 3 © # save a copy in register Ù # remove duplicates v # for each unique triplet yðý # join the triplet by spaces ®y¢ # count the triplets occurrences in the list saved to register ‚ # pair the string with the count ˆ # push it to global list # output global list at the end of the program  # Python, 143 bytes lambda s:Counter(tuple(''.join(filter(str.isalnum,w))for w in s.lower().split())[i:i+3]for i in range(s.count(' ')-1)) from collections import*  Try it online! Unnamed function which returns a dictionary (specifically a Counter object) which has keys that are tuples of the triples (in the correct order) and values that are their counts. • You can assume multiple consecutive spaces won't exist. – Carcigenicate Mar 5 '17 at 13:35 • Thanks; best to update the question to specify this I think. – Jonathan Allan Mar 5 '17 at 13:38 • will do. I literally just woke up though. – Carcigenicate Mar 5 '17 at 13:38 # Bash + Unix utilities, 117 bytes s=[^\ ]* p=s/^$s\ //
tr A-Z a-z|tr -cd \ a-z0-9|sed "p;$p;p;$p"|sed "s/$$s s s$$ /\1\n/g"|grep ' .* '|sort|uniq -c


Try it online!

Input on stdin, output on stdout.

Sample run:

Input:

Oh hi there guy. What's up? Oh hi there.


Output:

      1 guy whats up
1 hi there guy
2 oh hi there
1 there guy whats
1 up oh hi
1 whats up oh


# JavaScript (ES6), 124

s=>s.toLowerCase().replace(/\S+/g,x=>t.push(x.replace(/[\W_]/g,''))>2&&(k[t]=-~k[t],t.shift()),t=[],k={})&&JSON.stringify(k)


Note that the output shows the frequency of each triplet, as requested. If the output could just contain the frequency data in a machine usable format, I could save 16 bytes of JSON.stringify()

Less golfed

s => {
s = s.toLowerCase() // force to lowercase
t = [] // init current group (or triplet)
k = {} // init frequency list
s.replace(/\S+/g, x => ( // execute for each sequence of non space chars
x = x.replace(/[\W_]/g,''), //remove non alphanumeric chars
t.push(x) > 2 // add to end of current group
&& (          // if there are 3 elements in group (it's a triplet)
k[t] = -~k[t], // add to frequency list incrementing count
t.shift() // remove first element of current group
)
)
return JSON.stringify(k) // return in human readable format
}


Test

let F=
s=>s.toLowerCase().replace(/\S+/g,x=>t.push(x.replace(/[\W_]/g,''))>2&&(k[t]=-~k[t],t.shift()),t=[],k={})&&JSON.stringify(k)

;["Oh hi there guy. What's up? Oh hi there."
,"aa aa aa aa"
,"aa bb a bb a bb a cc a bb a"
,"99 bottles of beer"
,"There are two main types of chatbots, one functions based on a set of rules, and the other more advanced version uses artificial intelligence. The chatbots based on rules, tend to be limited in functionality, and are as smart as they are programmed to be. On the other end, a chatbot that uses artificial intelligence, understands language, not just commands, and continuously gets smarter as it learns from conversations it has with people."]
.forEach(t=>{
O.textContent += t + '\n'+ F(t).replace(/,"/g,'\n"')+'\n\n'
})
<pre id=O></pre>

• A JSON should be an acceptable output unless it contains a bunch of other irrelevant data. – Carcigenicate Mar 6 '17 at 14:56
• @Carcigenicate JSON is a way to encode data in a stream of characters, and it' human readable. And JSON is exactly the output right now, and as you can see, it only contains relevant data. – edc65 Mar 6 '17 at 15:42

# JavaScript (ES6), 200198 187 bytes

(-2 bytes thanks to Luke)

(-11 bytes thanks to edc65)

H=(h,k=[],i=0)=>i+2<(z=h.toLowerCase().replace(/[^\w ]|_/g,'').split ).length?H(h,k.concat(z.slice(i,i+3)+''),-~i):new Set(k.map(a=>a+':'+k.filter(b=>b==a).length)).forEach(m=>alert(m))


A recursive solution. Takes input as a single string and outputs dialog boxes containing each triplet's count in the format <Triplet>:<Count>. As always, golfing tips are greatly appreciated.

## Test Snippet

H=(h,k=[],i=0)=>i+2<(z=h.toLowerCase().replace(/[^\w ]|_/g,'').split ).length?H(h,k.concat(z.slice(i,i+3)+''),-~i):new Set(k.map(a=>a+':'+k.filter(b=>b==a).length)).forEach(m=>alert(m))
<input type="text" id="i" value="Oh hi there guy._ What's up? Oh hi there."></input>
<input type="button" value="Submit" onclick="H(document.getElementById('i').value)"></input>

• split(" ") can become split. I don't really understand your Regex. Don't you want to replace /\W|_/ (anything that is (not a word character) or (an underscore))? The .forEach() at the end can probably be replaced by map(). – Luke Mar 5 '17 at 6:51
• @Luke About the Regex, I also don't want to replace spaces (which are apparently referred to as non-word characters) so I can use .split(" ") on the result. As for your map() advice, the Set() object does not seem to have Set.prototype.map() defined according to Set.prototype, so that won't work. Still, thanks for your first tip. – R. Kap Mar 5 '17 at 8:29
• [z[i],z[i+1],z[i+2]].join' ' -> z[i]+' '+z[i+1]+' '+z[i+2], but I don't see a reason to use a space to separate words, so why not z.slice(i,i+3)+'' – edc65 Mar 5 '17 at 22:23
• @edc65 Because I am just starting out with JavaScript and did not really know/remember that built-in existed. Anyways, thanks for the tip! :) – R. Kap Mar 5 '17 at 23:51

# Python 3, 151 149 bytes

s="".join(c for c in input()if c.isalnum()or c==" ").lower().split()
k=[tuple(s[i:i+3])for i in range(len(s)-2)]
print(*((x,k.count(x))for x in{*k}))


Try it online!

# CJam, 25 bytes

qel"␡{a:0!":,:^-S/3ew$ep  ␡ represents a Delete character (ASCII 127). Try it online! Explanation q Read all input el Convert it to all lowercase "␡{a:0!" Push this string :, Map each character in the string to the range of chars up to, but excluding it :^ Reduce the array of ranges with symmetric set differences The result will be an array containing all disallowed characters - Remove all instances of these characters from the input S/ Split the input on spaces 3ew Take overlapping slices of length 3$     Sort them
e   Take the run-length encoding
p  Print it


# Python 2, 164 159 bytes

s=filter(lambda k:k.isalnum()or k==' ',input()).lower().split()
k=[tuple(s[i:i+3])for i in range(len(s)-3)]
l={}
for i in k:l[i]=l[i]+1if i in l else 1
print l


Not a really great solution, I know. Pretty straightforward.

Thanks to @ovs for saving 5 bytes overall.

• Some improvements: ' ' is the default parameter of split, you don't need the newline and tab after for and you can replace your conditional expression with l[i]=l.get(i,0)+1 Tio – ovs Mar 5 '17 at 7:35
• @ovs Oh, nice. I didn't notice. Thanks! – HyperNeutrino Mar 8 '17 at 14:54

# R, 114 bytes

x=readline();y=strsplit(tolower(gsub('[[:punct:]]','',x)),' ')[];s=3:length(y);table(paste(y[s-2],y[s-1],y[s]))
`

Reads input, removes punctuation, splits it into words and counts triplets.