I'm doing an exercise that requires me to write a function that capitalize all the first letters of the words of a string.
Here is what I did so far:
upperFirst:: String -> String
upperFirst str = let
upperFirstForEachWord (firstLetter:others) = toUpper firstLetter : map toLower others
in unwords (map upperFirstForEachWord (words str))
And this is how it works:
upperFirst "" = ""
upperFirst "hello friends!" = "Hello Friends!"
But also:
upperFirst " " = ""
upperFirst " a a a " = "A A A"
I'm losing the white spaces at the beginning, at the end and the double ones because of the function words
.
How can I keep them recursively (without working on all the possible cases)?
Thank you for any help!
Instead of extracting words, as words
does, you want to just split the string so each word beginning is also at the start of a list. As Hao Lian commented, this can be done with splitOn
, but the standard groupBy
will also do the trick:
import Data.List
import Data.Char
upperFirst = concat
. map (\(c:cs) -> toUpper c : cs)
. groupBy (\a b -> isSpace a == isSpace b)
How this works: it groups together chars which are either all spaces, or all non-spaces. It then uppercases the start of each substring (bit inefficient to also do that for the whitespace, but harmless) and then just concatenates all the strings back together.
As user3237465 remarks, the combination of map
and concat
is very common, and a special case of something even more common . Furthermore, there's a nice little combinator that comes in handy when grouping or sorting by some predicate. Hence you can also write this as
import Control.Monad
import Data.Function
upperFirst = groupBy ((==)`on`isSpace) >=> \(c:cs) -> toUpper c : cs
To top it up, you can use the hip modern way of modifying things, for the uppercasing part:
import Control.Lens
upperFirst = groupBy ((==)`on`isSpace) >=> ix 0 %~ toUpper
Pattern matching is your friend here
import Data.Char
upperFirst :: String -> String
upperFirst (c1:c2:rest) =
if isSpace c1 && isLower c2
then c1 : toUpper c2 : upperFirst rest
else c1 : upperFirst (c2:rest)
upperFirst s = s
The only problem with this function is that the first character won't get capitalized (also affects single-character strings), but if you really need that functionality then just wrap the call to this function up in another that handles those special cases:
upperFirst' = <the implementation above>
upperFirst [] = []
upperFirst [c] = [toUpper c] -- No-op on non-letters
upperFirst (s:str) = upperFirst' (toUpper s:str)
To test:
> upperFirst "this is a test of \t this\n function"
"This Is A Test Of \t This\n Function"
import Data.Char
upperFirst :: String -> String
upperFirst s = zipWith upper (' ':s) s where
upper c1 c2 | isSpace c1 && isLower c2 = toUpper c2
upper c1 c2 = c2
Eg
upperFirst "a test for upperFirst"
reduces to
zipWith upper
" a test for upperFirst "
"a test for upperFirst "
If a symbol in the first string is a space and a symbol at the same position in the second string is a lowercase letter, then make it uppercase, otherwise return the same. So
upper ' ' 'a' = 'A'
upper 'a' ' ' = ' '
upper ' ' 't' = 'T'
upper 't' 'e' = 'e'
and so on. Hence the result is "A Test For UpperFirst "
.
This problem is a prototypical scan:
>>> scanl (\old new -> if isSpace old then toUpper new else new) ' ' " hello world "
" Hello World "
The initial state of the 'seed' -- here a space -- is added at the beginning, but tail is legitimate here. See the implementation of scanl
here , and compare it with the other scan functions, which have different advantages. Here's a simple version:
scan op state [] = state : []
scan op state (x:xs) = state : scan op (op state x) xs
You can write your function by inlining
op old new = if isSpace old then toUpper new else new
into the definition
myscan state [] = state : []
myscan state (x:xs) = state : myscan (if isSpace state then toUpper x else x) xs
starting it with whitespace and then taking the tail:
titlecase = tail . myscan ' '
Then in ghci I see
>>> titlecase " hello world "
" Hello World "
Or you can just directly use the general scan
we defined
>>> let op old new = if isSpace old then toUpper new else new
>>> scan op ' ' " hello world "
" Hello World "
>>> tail $ scan op ' ' " hello world "
" Hello World "
This is very similar to Michael's solution , but I think mapAccumL
is a better fit than scanl
:
upperFirst :: Traversable t => t Char -> t Char
upperFirst = snd . mapAccumL go True where
go lastSpace x
| isSpace x = (True, x)
| lastSpace = (,) False $! toUpper x
| otherwise = (False, x)
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.