@@ -42,6 +42,7 @@ module Data.Text.Fusion.Common
4242 -- $case
4343 , toCaseFold
4444 , toLower
45+ , toTitle
4546 , toUpper
4647
4748 -- ** Justification
@@ -102,13 +103,15 @@ module Data.Text.Fusion.Common
102103
103104import Prelude (Bool(..), Char, Eq(..), Int, Integral, Maybe(..),
104105 Ord(..), Ordering(..), String, (.), ($), (+), (-), (*), (++),
105− (&&), fromIntegral, otherwise)
106+ (&&), fromIntegral, not, otherwise)
106107import qualified Data.List as L
107108import qualified Prelude as P
108109import Data.Bits (shiftL)
110+import Data.Char (isLetter)
109111import Data.Int (Int64)
110112import Data.Text.Fusion.Internal
111−import Data.Text.Fusion.CaseMapping (foldMapping, lowerMapping, upperMapping)
113+import Data.Text.Fusion.CaseMapping (foldMapping, lowerMapping, titleMapping,
114+ upperMapping)
112115import Data.Text.Fusion.Size
113116import GHC.Prim (Addr#, chr#, indexCharOffAddr#, ord#)
114117import GHC.Types (Char(..), Int(..))
@@ -432,6 +435,38 @@ toLower :: Stream Char -> Stream Char
432435toLower = caseConvert lowerMapping
433436{-# INLINE [0] toLower #-}
434437
438+-- | /O(n)/ Convert a string to title case, using simple case
439+-- conversion.
440+--
441+-- Unlike the other case conversion functions, this function does not
442+-- convert every letter of its input. Instead, the first letter is
443+-- converted to title case, as is every subsequent letter that
444+-- immediately follows a non-letter.
445+--
446+-- The result string may be longer than the input string. For example,
447+-- the Latin small ligature fl (U+FB02) is converted to the
448+-- sequence Latin capital letter F (U+0046) followed by Latin small
449+-- letter l (U+006C).
450+--
451+-- /Note/: this function does not take language or culture specific
452+-- rules into account. For instance, in English, different style
453+-- guides disagree on whether the book name \"The Hill of the Red
454+-- Fox\" is correctly title cased—but this function will
455+-- capitalize /every/ word.
456+toTitle :: Stream Char -> Stream Char
457+toTitle (Stream next0 s0 len) = Stream next (CC (False :*: s0) '\0' '\0') len
458+ where
459+ next (CC (letter :*: s) '\0' _) =
460+ case next0 s of
461+ Done -> Done
462+ Skip s' -> Skip (CC (letter :*: s') '\0' '\0')
463+ Yield c s'
464+ | not letter && letter' -> titleMapping c (letter' :*: s')
465+ | otherwise -> Yield c (CC (letter' :*: s') '\0' '\0')
466+ where letter' = isLetter c
467+ next (CC s a b) = Yield a (CC s b '\0')
468+{-# INLINE [0] toTitle #-}
469+
435470justifyLeftI :: Integral a => a -> Char -> Stream Char -> Stream Char
436471justifyLeftI k c (Stream next0 s0 len) =
437472 Stream next (s0 :*: S1 :*: 0) (larger (fromIntegral k) len)