"Exercise: WordCount"

wordCount: aString
"Your task is to count how many times each word occurs in a subtitle of a drama.
    The subtitles from these dramas use only ASCII characters.
    The characters often speak in casual English, using contractions like they're or it's.
    Though these contractions come from two words (e.g. we are), the contraction (we're) is considered a single word.
    Words can be separated by any form of punctuation (e.g. :, !, or ?) or whitespace (e.g. \t, \n, or ' ').
    The only punctuation that does not separate words is the apostrophe in contractions.
    Numbers are considered words.
    If the subtitles say It costs 100 dollars. then 100 will be its own word.
    Words are case insensitive.
    The ordering of the word counts in the results doesn't matter."
"(self new wordCount: ('That''s the password: ''PASSWORD 123''!', String lf, ' cried the Special Agent.', String lf, ' So I fled.')) >>> ((Dictionary new) add: ('123'->1); add: ('agent'->1); add: ('cried'->1); add: ('fled'->1); add: ('i'->1); add: ('password'->2); add: ('so'->1); add: ('special'->1); add: ('that''s'->1); add: ('the'->2); yourself)"
| bag current ch prevIsAlnum nextIsAlnum isInteriorApostrophe |
	bag := Bag new.
	current := WriteStream on: (String new: aString size).

	1 to: aString size do: [ :i |
			ch := aString at: i.
			prevIsAlnum := i > 1 and: [ (aString at: i - 1) isAlphaNumeric ].
			nextIsAlnum := i < aString size and: [
				               (aString at: i + 1) isAlphaNumeric ].
			isInteriorApostrophe := ch = $' and: [
				                        prevIsAlnum and: [ nextIsAlnum ] ].

			(ch isAlphaNumeric or: [ isInteriorApostrophe ])
				ifTrue: [ current nextPut: ch asLowercase ]
				ifFalse: [
						current position > 0 ifTrue: [
								bag add: current contents.
								current reset ] ] ].

	current position > 0 ifTrue: [ bag add: current contents ].

	^ bag valuesAndCounts


"Tests"
testWordCount
| result |

  result := task wordCount: 'word'.
  self assert: result equals: ((Dictionary new) add: ('word'->1); yourself).

  result := task wordCount: 'one of each'.
  self assert: result equals: ((Dictionary new) add: ('of'->1); add: ('each'->1); add: ('one'->1); yourself).

  result := task wordCount: 'one fish two fish red fish blue fish'.
  self assert: result equals: ((Dictionary new) add: ('blue'->1); add: ('fish'->4); add: ('one'->1); add: ('red'->1); add: ('two'->1); yourself).

  result := task wordCount: 'one,two,three'.
  self assert: result equals: ((Dictionary new) add: ('two'->1); add: ('one'->1); add: ('three'->1); yourself).

  result := task wordCount: 'one,
 two,
 three'.
  self assert: result equals: ((Dictionary new) add: ('two'->1); add: ('one'->1); add: ('three'->1); yourself).

  result := task wordCount: 'car: carpet as java: javascript!!&@$%^&'.
  self assert: result equals: ((Dictionary new) add: ('as'->1); add: ('javascript'->1); add: ('java'->1); add: ('car'->1); add: ('carpet'->1); yourself).

  result := task wordCount: 'testing, 1, 2 testing'.
  self assert: result equals: ((Dictionary new) add: ('testing'->2); add: ('1'->1); add: ('2'->1); yourself).

  result := task wordCount: 'go Go GO Stop stop'.
  self assert: result equals: ((Dictionary new) add: ('stop'->2); add: ('go'->3); yourself).

  result := task wordCount: 'First: don''t laugh. Then: don''t cry.'.
  self assert: result equals: ((Dictionary new) add: ('cry'->1); add: ('don''t'->2); add: ('laugh'->1); add: ('first'->1); add: ('then'->1); yourself).

  result := task wordCount: 'Joe can''t tell between ''large'' and large.'.
  self assert: result equals: ((Dictionary new) add: ('large'->2); add: ('joe'->1); add: ('can''t'->1); add: ('and'->1); add: ('tell'->1); add: ('between'->1); yourself).

  result := task wordCount: ' multiple   whitespaces'.
  self assert: result equals: ((Dictionary new) add: ('multiple'->1); add: ('whitespaces'->1); yourself).

  result := task wordCount: ',
 ,one,
  ,two 
  ''three'''.
  self assert: result equals: ((Dictionary new) add: ('two'->1); add: ('one'->1); add: ('three'->1); yourself)