简体   繁体   中英

Haskell - Capitalize the first letter of each word in a string without losing white spaces

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM