diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+7.0.0
+
+* Introduced `Formatting.Combinators`.
+* Fixed: #62 and #60: incorrect formatting of Integral types that do not have negative values (e.g. Word)
+* Fixed: #59 rendering of floats e.g. 0.01 as "0.01" rather than "1.0e-2"
+* Added dependency of double-conversion to provide fast and correct rendering of floating-point numbers (including the fix for #59).
+* Make compatible with bytestring-0.11.0.0
+* Removed -O2 ghc flag
+* Updated .cabal file version from 1.8 to 2.4
+
 6.3.7
 
 * Introduced instance `Buildable a => Buildable [a]`.
@@ -27,8 +37,8 @@
   * `prec`
   * `expt`
 * Added a test suite with regression tests:
-  * Fixed: https://github.com/chrisdone/formatting/issues/31
-  * Fixed: https://github.com/chrisdone/formatting/issues/28
+  * Fixed: #31
+  * Fixed: #28
   * Fixed: https://github.com/bos/text-format/issues/18
 
 6.2.5
diff --git a/formatting.cabal b/formatting.cabal
--- a/formatting.cabal
+++ b/formatting.cabal
@@ -1,22 +1,64 @@
+cabal-version:       2.4
 name:                formatting
-version:             6.3.7
+version:             7.0.0
 synopsis:            Combinator-based type-safe formatting (like printf() or FORMAT)
 description:         Combinator-based type-safe formatting (like printf() or FORMAT), modelled from the HoleyMonoids package.
-license:             BSD3
+bug-reports:         https://github.com/AJChapman/formatting/issues
+license:             BSD-3-Clause
 license-file:        LICENSE
-author:              Chris Done, Shachaf Ben-Kiki, Martijn van Steenbergen, Mike Meyer, Bryan O'Sullivan
-maintainer:          chrisdone@gmail.com
-copyright:           2013 Chris Done, Shachaf Ben-Kiki, Martijn van Steenbergen, Mike Meyer, 2011 MailRank, Inc.
+author:              Chris Done, Shachaf Ben-Kiki, Martijn van Steenbergen, Mike Meyer, Bryan O'Sullivan, Alex Chapman
+maintainer:          alex@farfromthere.net
+copyright:           2013 Chris Done, Shachaf Ben-Kiki, Martijn van Steenbergen, Mike Meyer, 2011 MailRank, Inc., 2020 Alex Chapman
 category:            Text
 build-type:          Simple
-cabal-version:       >=1.8
 extra-source-files:  CHANGELOG.md
 
+common deps
+  build-depends:
+    base >= 4.5 && < 5,
+    text >= 0.11.0.8,
+    time,
+    old-locale,
+    scientific >= 0.3.0.0,
+    clock >= 0.4,
+    array,
+    ghc-prim,
+    text >= 0.11.0.8,
+    transformers,
+    bytestring >=0.10.4,
+    integer-gmp >= 0.2,
+    double-conversion ^>= 2.0.2.0
+
+  if !impl(ghc >= 8.0)
+    build-depends: semigroups
+
+-- Warnings list list taken from
+-- https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3
+-- Enable all warnings with -Weverything, then disable the ones we
+-- don’t care about
+  default-language:    Haskell2010
+  ghc-options:         -Weverything
+                       -Wno-all-missed-specialisations
+                       -Wno-implicit-prelude
+                       -Wno-missed-specialisations
+                       -Wno-missing-exported-signatures
+                       -Wno-missing-import-lists
+                       -Wno-missing-local-signatures
+                       -Wno-monomorphism-restriction
+                       -Wno-missing-deriving-strategies
+                       -Wno-safe
+                       -Wno-unsafe
+                       -fprint-potential-instances
+  cpp-options: -DINTEGER_GMP
+
 library
+  import: deps
+
   exposed-modules:
     Formatting
     Formatting.Formatters
     Formatting.ShortFormatters
+    Formatting.Combinators
     Formatting.Examples
     Formatting.Time
     Formatting.Clock
@@ -30,32 +72,16 @@
     Data.Text.Format
     Data.Text.Format.Int
 
-  build-depends:
-    base >= 4.5 && < 5,
-    text >= 0.11.0.8,
-    time,
-    old-locale,
-    scientific >= 0.3.0.0,
-    clock >= 0.4,
-    array,
-    ghc-prim,
-    text >= 0.11.0.8,
-    transformers,
-    bytestring >=0.10.4,
-    integer-gmp >= 0.2,
-    semigroups
-
   hs-source-dirs:    src
-  ghc-options:       -O2
-  cpp-options: -DINTEGER_GMP
 
 test-suite formatting-test
+  import:              deps
+  build-depends:       formatting, hspec
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
   main-is:             Spec.hs
-  build-depends:       base, formatting, hspec, semigroups, text
-  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
 
 source-repository head
   type:     git
-  location: http://github.com/chrisdone/formatting
+  location: http://github.com/AJChapman/formatting
diff --git a/src/Data/Text/Format.hs b/src/Data/Text/Format.hs
--- a/src/Data/Text/Format.hs
+++ b/src/Data/Text/Format.hs
@@ -23,22 +23,12 @@
     , shortest
     ) where
 
-import           Control.Monad.IO.Class (MonadIO(liftIO))
-import qualified Data.ByteString.Lazy as L
-import qualified Data.ByteString.Lazy.Builder as L
-import           Data.Text (Text)
-import qualified Data.Text as ST
-import qualified Data.Text as T
+import           Data.Double.Conversion.Text
 import qualified Formatting.Buildable as B
-import qualified Data.Text.Encoding as T
-import           Data.Text.Format.Functions ((<>))
-import           Data.Text.Format.Types (Shown(..), Hex(..))
+import           Data.Text.Format.Types (Hex(..))
 import qualified Data.Text.Lazy as LT
 import           Data.Text.Lazy.Builder
-import qualified Data.Text.Lazy.IO as LT
 import           Prelude hiding (exp, print)
-import           System.IO (Handle)
-import           Text.Printf
 
 -- | Pad the left hand side of a string until it reaches @k@
 -- characters wide, if necessary filling with character @c@.
@@ -58,14 +48,14 @@
          Int
       -- ^ Number of digits of precision after the decimal.
       -> a -> Builder
-fixed decs = B.build . T.pack . (printf ("%." ++ show decs ++ "f") :: Double->String) . realToFrac
+fixed decs = fromText . toFixed decs . realToFrac
 {-# NOINLINE[0] fixed #-}
 
 -- | Render a floating point number using the smallest number of
 -- digits that correctly represent it.
-shortest :: (Real a) => a -> Builder
-shortest = B.build . T.decodeUtf8 . L.toStrict . L.toLazyByteString . L.doubleDec . realToFrac
-{-# NOINLINE[0] shortest #-}
+shortest :: Real a => a -> Builder
+shortest = fromText . toShortest . realToFrac
+{-# INLINE shortest #-}
 
 -- | Render an integer using hexadecimal notation.  (No leading "0x"
 -- is added.)
diff --git a/src/Data/Text/Format/Functions.hs b/src/Data/Text/Format/Functions.hs
--- a/src/Data/Text/Format/Functions.hs
+++ b/src/Data/Text/Format/Functions.hs
@@ -18,7 +18,6 @@
     , i2d
     ) where
 
-import Data.Monoid (mappend)
 import Data.Text.Lazy.Builder (Builder)
 #if MIN_VERSION_base(4,11,0)
 import Prelude hiding ((<>))
diff --git a/src/Data/Text/Format/Int.hs b/src/Data/Text/Format/Int.hs
--- a/src/Data/Text/Format/Int.hs
+++ b/src/Data/Text/Format/Int.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE BangPatterns, CPP, MagicHash, UnboxedTuples #-}
+{-# LANGUAGE BangPatterns, CPP, MagicHash, UnboxedTuples, ScopedTypeVariables #-}
 
 -- Module:      Data.Text.Format.Int
 -- Copyright:   (c) 2011 MailRank, Inc.
@@ -18,13 +18,11 @@
     ) where
 
 import Data.Int (Int8, Int16, Int32, Int64)
-import Data.Monoid (mempty)
 import Data.Text.Format.Functions (i2d)
 import qualified Data.Text.Format.Functions as F ((<>))
 import Data.Text.Lazy.Builder
-import Data.Word (Word, Word8, Word16, Word32, Word64)
+import Data.Word (Word8, Word16, Word32, Word64)
 import GHC.Base (quotInt, remInt)
-import GHC.Num (quotRemInteger)
 import GHC.Types (Int(..))
 
 #ifdef  __GLASGOW_HASKELL__
@@ -41,7 +39,7 @@
 # define PAIR(a,b) (a,b)
 #endif
 
-decimal :: (Integral a, Bounded a) => a -> Builder
+decimal :: forall a. (Integral a, Bounded a) => a -> Builder
 {-# SPECIALIZE decimal :: Int -> Builder #-}
 {-# SPECIALIZE decimal :: Int8 -> Builder #-}
 {-# SPECIALIZE decimal :: Int16 -> Builder #-}
@@ -54,7 +52,7 @@
 {-# SPECIALIZE decimal :: Word64 -> Builder #-}
 {-# RULES "decimal/Integer" decimal = integer 10 :: Integer -> Builder #-}
 decimal i
-    | i == minBound =
+    | (minBound :: a) < 0 && i == minBound =
         -- special case, since (-i) would not be representable assuming two's
         -- compliment:
         minus F.<> integer 10 (negate $ fromIntegral i)
diff --git a/src/Data/Text/Format/Types.hs b/src/Data/Text/Format/Types.hs
--- a/src/Data/Text/Format/Types.hs
+++ b/src/Data/Text/Format/Types.hs
@@ -19,11 +19,6 @@
     , Hex(..)
     ) where
 
-import Data.Monoid (Monoid(..))
-import Data.String (IsString(..))
-import Data.Text (Text)
-import Data.Typeable (Typeable)
-
 -- | Render an integral type in hexadecimal.
 newtype Hex a = Hex a
     deriving (Eq, Ord, Read, Show, Num, Real, Enum, Integral)
diff --git a/src/Formatting.hs b/src/Formatting.hs
--- a/src/Formatting.hs
+++ b/src/Formatting.hs
@@ -1,12 +1,11 @@
 {-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE OverloadedStrings #-}
 {-# OPTIONS -Wall #-}
 
 -- |
 -- Module      : Text.Format
 -- Copyright   : (c) 2013 Chris Done, 2013 Shachaf Ben-Kiki
 -- License     : BSD3
--- Maintainer  : chrisdone@gmail.com
+-- Maintainer  : alex@farfromthere.net
 -- Stability   : experimental
 -- Portability : GHC
 --
@@ -16,7 +15,8 @@
 --
 -- >>> format ("Person's name is " % text % ", age is " % hex) "Dave" 54
 --
--- See "Formatting.Formatters" for a complete list of formatting combinators.
+-- See "Formatting.Formatters" for a list of formatters.
+-- See "Formatting.Combinators" for a list of formatting combinators, for combining and altering formatters.
 
 module Formatting
   (
@@ -31,12 +31,17 @@
   format,
   sformat,
   bprint,
+  bformat,
   fprint,
+  fprintLn,
   hprint,
+  hprintLn,
   formatToString,
   -- * Formatting library
-  module Formatting.Formatters
+  module Formatting.Formatters,
+  module Formatting.Combinators
  ) where
 
 import Formatting.Formatters
+import Formatting.Combinators
 import Formatting.Internal
diff --git a/src/Formatting/Buildable.hs b/src/Formatting/Buildable.hs
--- a/src/Formatting/Buildable.hs
+++ b/src/Formatting/Buildable.hs
@@ -17,11 +17,9 @@
     ) where
 
 #if MIN_VERSION_base(4,8,0)
-import qualified Data.ByteString.Lazy as L
 import           Data.Void (Void, absurd)
 #endif
 
-import           Data.Monoid (mempty, mconcat)
 import           Data.Int (Int8, Int16, Int32, Int64)
 import           Data.Fixed (Fixed, HasResolution, showFixed)
 import           Data.List (intersperse)
@@ -31,15 +29,12 @@
 import           Data.Text.Format.Types (Hex(..), Shown(..))
 import           Data.Text.Lazy.Builder
 import           Data.Time.Calendar (Day, showGregorian)
-import           Data.Time.Clock (DiffTime, NominalDiffTime, UTCTime, UniversalTime)
-import           Data.Time.Clock (getModJulianDate)
+import           Data.Time.Clock (getModJulianDate, DiffTime, NominalDiffTime, UTCTime, UniversalTime)
 import           Data.Time.LocalTime (LocalTime, TimeOfDay, TimeZone, ZonedTime)
-import           Data.Word (Word, Word8, Word16, Word32, Word64)
+import           Data.Word (Word8, Word16, Word32, Word64)
 import           Foreign.Ptr (IntPtr, WordPtr, Ptr, ptrToWordPtr)
 import qualified Data.Text as ST
 import qualified Data.Text.Lazy as LT
-import qualified Data.Text.Encoding as T
-import qualified Data.ByteString.Lazy.Builder as L
 
 -- | The class of types that can be rendered to a 'Builder'.
 class Buildable p where
@@ -121,7 +116,7 @@
     build = decimal
     {-# INLINE build #-}
 
-instance (Integral a, Buildable a) => Buildable (Ratio a) where
+instance Buildable a => Buildable (Ratio a) where
     {-# SPECIALIZE instance Buildable (Ratio Integer) #-}
     build a = build (numerator a) F.<> singleton '/' F.<> build (denominator a)
 
diff --git a/src/Formatting/Combinators.hs b/src/Formatting/Combinators.hs
new file mode 100644
--- /dev/null
+++ b/src/Formatting/Combinators.hs
@@ -0,0 +1,648 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Formatting.Combinators
+Copyright   : (c) 2020 Alex Chapman
+License     : BSD3
+Maintainer  : alex@farfromthere.net
+Stability   : experimental
+Portability : GHC
+Description : Formatting combinators for building new formatters, with some useful pre-defined formatters.
+
+A formatting combinator takes a Format and returns another Format.
+Generally we want to change what the original format takes as its *input*,
+leaving the output polymorphic.
+Many of these combinators can be chained together to form a single 'Format'.
+
+Implementation detail: in order to be able to chain multiple combinators to make a single 'Format' we need them all to use the same intermediate string type, and we have chosen 'Builder'.
+This does not tie you to using 'Builder's, because the final output string type 'r' is still polymorphic.
+|-}
+module Formatting.Combinators
+  (
+  -- * Formatting lists of data
+    concatenated
+  , joinedWith
+  , intercalated
+  , unworded
+  , unlined
+  , spaced
+  , commaSep
+  , commaSpaceSep
+  , list
+  , qlist
+  , took
+  , dropped
+
+  -- * Splitting strings to pass to other formatters
+  , splat
+  , splatWith
+  , splatOn
+  , worded
+  , lined
+
+  -- * Altering formatted strings
+  , alteredWith
+  , replaced
+  , uppercased
+  , lowercased
+  , titlecased
+  , ltruncated
+  , ctruncated
+  , rtruncated
+  , lpadded
+  , rpadded
+  , cpadded
+  , lfixed
+  , rfixed
+  , cfixed
+
+  -- * Wrapping formatted strings
+  , prefixed
+  , suffixed
+  , surrounded
+  , enclosed
+  , squoted
+  , dquoted
+  , parenthesised
+  , squared
+  , braced
+  , angled
+  , backticked
+
+  -- * Changing indentation
+  , indented
+  , indentedLines
+  , reindented
+
+  -- * Numerical adapters
+  , roundedTo
+  , truncatedTo
+  , ceilingedTo
+  , flooredTo
+
+  -- * Structure formatting
+  , viewed
+  , accessed
+
+  -- * Fixed-width number formatting
+  , binPrefix
+  , octPrefix
+  , hexPrefix
+  ) where
+
+import Control.Category ((>>>))
+import Data.Foldable (toList)
+import Data.Function ((&))
+import Data.Functor.Const (Const(..), getConst)
+import Data.Int (Int64)
+import Data.Text.Lazy (Text)
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy.Builder (Builder)
+import qualified Data.Text.Lazy.Builder as TLB
+import Formatting.Internal
+import Formatting.Formatters
+
+-- | Format each value in a list and concatenate them all:
+--
+-- >>> format (concatenated text) ["one", "two", "three"]
+-- "onetwothree"
+--
+-- >>> format (took 15 (concatenated bin)) [1..]
+-- "1101110010111011110001001101010111100110111101111"
+concatenated :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+concatenated f = later $ foldMap (bprint f)
+
+-- | Use the given text-joining function to join together the individually rendered items of a list.
+--
+-- >>> format (joinedWith (mconcat . reverse) int) [123, 456, 789]
+-- "789456123"
+joinedWith :: Foldable t => ([Text] -> Text) -> Format Builder (a -> Builder) -> Format r (t a -> r)
+joinedWith joiner f = later $ toList
+  >>> fmap (bprint f >>> TLB.toLazyText)
+  >>> joiner
+  >>> TLB.fromLazyText
+
+-- | Format each value in a list and place the given string between each:
+--
+-- >>> fprintLn (intercalated "||" int) [1, 2, 3]
+-- 1||2||3
+intercalated :: Foldable t => Text -> Format Builder (a -> Builder) -> Format r (t a -> r)
+intercalated s = joinedWith (TL.intercalate s)
+
+-- | Format each value in a list with spaces in between:
+--
+-- >>> format (unworded int) [1, 2, 3]
+-- "1 2 3"
+unworded :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+unworded = joinedWith TL.unwords
+
+-- | Format each value in a list, placing each on its own line:
+--
+-- >>> fprint (unlined char) ['a'..'c']
+-- a
+-- b
+-- c
+unlined :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+unlined = joinedWith TL.unlines
+
+-- | Separate the formatted items of the Foldable (e.g. list) with spaces:
+--
+-- >>> format (spaced int) [1, 2, 3]
+-- "1 2 3"
+--
+-- Note that this behaviour is identical to 'unworded', it's just a different way of thinking about it.
+spaced :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+spaced = intercalated " "
+
+-- | Separate the formatted items of the Foldable (e.g. list) with commas:
+--
+-- >>> format (commaSep stext) ["one", "two", "three", "four", "five"]
+-- "one,two,three,four,five"
+--
+-- >>> format (took 5 (commaSep int)) [1..]
+-- "1,2,3,4,5"
+commaSep :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+commaSep = intercalated ","
+
+-- | Separate the formatted items of the Foldable (e.g. list) with commas and spaces:
+--
+-- >>> format (took 3 (commaSpaceSep ords)) [1..]
+-- "1st, 2nd, 3rd"
+commaSpaceSep :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+commaSpaceSep = intercalated ", "
+
+-- | Add square brackets around the Foldable (e.g. a list), and separate each formatted item with a comma and space.
+--
+-- >>> format (list stext) ["one", "two", "three"]
+-- "[one, two, three]"
+--
+-- >>> format (list shown) ["one", "two", "three"]
+-- "[\"one\", \"two\", \"three\"]"
+list :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+list = commaSpaceSep >>> squared
+
+-- | Like 'list', but also put double quotes around each rendered item:
+--
+-- >>> fprintLn (qlist stext) ["one", "two", "three"]
+-- ["one", "two", "three"]
+qlist :: Foldable t => Format Builder (a -> Builder) -> Format r (t a -> r)
+qlist = dquoted >>> commaSpaceSep >>> squared
+
+-- | Take only the first n items from the list of items.
+--
+-- >>> format (took 7 (list bin)) [1..]
+-- "[1, 10, 11, 100, 101, 110, 111]"
+--
+-- >>> format (list bin) (take 7 [1..])
+-- "[1, 10, 11, 100, 101, 110, 111]"
+took :: Int -> Format r ([a] -> r) -> Format r ([a] -> r)
+took n = fmap (. take n)
+
+-- | Drop the first n items from the list of items.
+--
+-- >>> format (dropped 3 (list int) [1..6]
+-- "[4, 5, 6]"
+dropped :: Int -> Format r ([a] -> r) -> Format r ([a] -> r)
+dropped n = fmap (. drop n)
+
+-- | Utility for taking a text-splitting function and turning it into a formatting combinator.
+--
+-- >>> format (splatWith (TL.chunksOf 3) list int) 1234567890
+-- "[123, 456, 789, 0]"
+splatWith
+  :: (Text -> [Text]) -- ^ The text splitter
+  -> (Format r' (Builder -> r') -> Format Builder ([Builder] -> Builder)) -- ^ A list-formatting combinator, e.g. 'unworded', 'list', 'concatenated', etc.
+  -> Format r a -- ^ The base formatter, whose rendered text will be split
+  -> Format r a
+splatWith splitter lf f = later (TLB.toLazyText
+  >>> splitter
+  >>> fmap TLB.fromLazyText
+  >>> bprint (lf builder))
+  %. f
+
+-- | Split the formatted item in places the given predicated matches, and use the given list combinator to render the resultant list of strings
+-- (this function was sent to us from a parallel universe in which splat is the past participle of split, e.g. "whoops, I splat my pants").
+--
+-- >>> format (splat Data.Char.isSpace commaSpaceSep stext) "This\t  is\n\t\t  poorly formatted   "
+-- "This, , , is, , , , , poorly, formatted, , , "
+splat
+  :: (Char -> Bool) -- ^ Whether to split the string at this character
+  -> (Format r' (Builder -> r') -> Format Builder ([Builder] -> Builder)) -- ^ A list-formatting combinator, e.g. 'unworded', 'list', 'concatenated', etc.
+  -> Format r a -- ^ The base formatter, whose rendered text will be split
+  -> Format r a
+splat p = splatWith (TL.split p)
+
+-- | Split the formatted item at instances of the given string, and use the given list combinator to render the resultant list of strings.
+--
+-- >>> fprint (splatOn "," unlined text) "one,two,three"
+-- one
+-- two
+-- three
+--
+-- >>> fprint (splatOn "," indentedLines text) "one,two,three"
+--     one
+--     two
+--     three
+splatOn
+  :: Text -- ^ The text to split on
+  -> (Format r' (Builder -> r') -> Format Builder ([Builder] -> Builder)) -- ^ A list-formatting combinator, e.g. 'unworded', 'list', 'concatenated', etc.
+  -> Format r a -- ^ The base formatter, whose rendered text will be split
+  -> Format r a
+splatOn t = splatWith (TL.splitOn t)
+
+-- | Split the formatted item into words and use the given list combinator to render the resultant list of strings.
+--
+-- >>> format (worded list text) "one  two three  "
+-- "[one, two, three]"
+worded
+  :: (Format r' (Builder -> r') -> Format Builder ([Builder] -> Builder)) -- ^ A list-formatting combinator, e.g. 'unworded', 'list', 'concatenated', etc.
+  -> Format r a -- ^ The base formatter, whose rendered text will be split
+  -> Format r a
+worded = splatWith TL.words
+
+-- | Split the formatted item into lines and use the given list combinator to render the resultant list of strings.
+--
+-- >>> fprintLn (lined qlist text) "one two three\n\nfour five six\nseven eight nine\n\n"
+-- ["one two three", "", "four five six", "seven eight nine", ""]
+lined
+  :: (Format Builder (Builder -> Builder) -> Format Builder ([Builder] -> Builder)) -- ^ A list-formatting combinator, e.g. 'unworded', 'list', 'concatenated', etc.
+  -> Format r a -- ^ The base formatter, whose rendered text will be split
+  -> Format r a
+lined = splatWith TL.lines
+
+-- | Alter the formatted string with the given function.
+--
+-- >>> format (alteredWith Data.Text.Lazy.reverse int) 123456
+-- "654321"
+alteredWith :: (Text -> Text) -> Format r a -> Format r a
+alteredWith alterer f =
+  later (TLB.toLazyText >>> alterer >>> TLB.fromLazyText) %. f
+
+-- | Take a formatter and replace the given needle with the given replacement in its output.
+--
+-- >>> format (replaced "Bruce" "<redacted>" stext) "Bruce replied that Bruce's name was, in fact, '<redacted>'."
+-- "<redacted> replied that <redacted>'s name was, in fact, '<redacted>'."
+replaced :: Text -> Text -> Format r a -> Format r a
+replaced needle replacement = alteredWith (TL.replace needle replacement)
+
+-- | Convert any letters in the output of the given formatter to upper-case.
+--
+-- >>> format (uppercased text) "I'm not shouting, you're shouting."
+-- "I'M NOT SHOUTING, YOU'RE SHOUTING."
+uppercased :: Format r a -> Format r a
+uppercased = alteredWith TL.toUpper
+
+-- | Convert any letters in the output of the given formatter to lower-case.
+--
+-- >>> format (lowercased text) "Cd SrC/; Rm -Rf *"
+-- "cd src/; rm -rf *"
+lowercased :: Format r a -> Format r a
+lowercased = alteredWith TL.toLower
+
+-- | Convert the formatted string to title case, or something like it:
+--
+-- >>> format (titlecased string) "the life of brian"
+-- "The Life Of Brian"
+titlecased :: Format r a -> Format r a
+titlecased = alteredWith TL.toTitle
+
+-- | Truncate the formatted string at the end so that it is no more than the given number of characters in length, placing an ellipsis at the end such that it does not exceed this length.
+--
+-- >>> format (truncated 5 text) "hello"
+-- "hello"
+--
+-- >>> format (truncated 5 text) "hellos"
+-- "he..."
+ltruncated :: Int64 -> Format r a -> Format r a
+ltruncated n = ctruncated (n - 3) 0
+
+-- | Truncate the formatted string at the start so that it is no more than the given number of characters in length, placing an ellipsis at the start such that it does not exceed this length.
+--
+-- >>> format (rtruncated 5 text) "hello"
+-- "hello"
+--
+-- >>> format (rtruncated 5 text) "hellos"
+-- "...os"
+rtruncated :: Int64 -> Format r a -> Format r a
+rtruncated n = ctruncated 0 (n - 3)
+
+-- | Truncate the formatted string in the center, leaving the given number of characters at the start and end, and placing an ellipsis in between.
+-- The length will be no longer than `start + end + 3` characters long.
+-- 
+-- >>> format (ctruncated 15 4 text) "The quick brown fox jumps over the lazy dog."
+-- "The quick brown...dog."
+--
+-- >>> format (ctruncated 15 4 text) "The quick brown fox"
+-- "The quick brown fox"
+ctruncated :: Int64 -> Int64 -> Format r a -> Format r a
+ctruncated start end = alteredWith shorten
+  where
+    shorten :: Text -> Text
+    shorten txt =
+      let n = start + end + 3
+      in if TL.length txt <= n
+        then txt
+        else TL.take start txt <> "..." <> TL.takeEnd end txt
+
+-- | Pad the formatted string on the left with the given character to give it the given minimum width:
+--
+-- >>> format (lpadded 7 ' ' int) 1
+-- "      1"
+--
+-- >>> format (lpadded 7 ' ' int) 123456789
+-- "123456789"
+lpadded :: Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+lpadded i c = alteredWith (TL.justifyRight i c)
+
+-- | Pad the formatted string on the right with the given character to give it the given minimum width:
+--
+-- >>> format (rpadded 7 ' ' int) 1
+-- "1      "
+rpadded :: Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+rpadded i c = alteredWith (TL.justifyLeft i c)
+
+-- | Pad the formatted string on the left and right with the given character to center it, giving it the given minimum width:
+--
+-- >>> format (cpadded 7 ' ' int) 1
+-- "   1   "
+cpadded :: Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+cpadded i c = alteredWith (TL.center i c)
+
+-- | Format the item with a fixed width, padding with the given character on the left to extend, adding an ellipsis on the right to shorten:
+--
+-- >>> format (lfixed 10 ' ' int) 123
+-- "123       "
+--
+-- >>> format (lfixed 10 ' ' int) 1234567890
+-- "1234567890"
+--
+-- >>> format (lfixed 10 ' ' int) 123456789012345
+-- "1234567..."
+lfixed :: Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+lfixed n c = ltruncated n . rpadded n c
+
+-- | Format the item with a fixed width, padding with the given character on the right to extend, adding an ellipsis on the right to shorten:
+--
+-- >>> format (rfixed 10 ' ' int) 123
+-- "       123"
+--
+-- >>> format (rfixed 10 ' ' int) 1234567890
+-- "1234567890"
+--
+-- >>> format (rfixed 10 ' ' int) 123456789012345
+-- "...9012345"
+rfixed :: Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+rfixed n c = rtruncated n . lpadded n c
+
+-- | Format the item with a fixed width, padding with the given character on either side to extend, adding an ellipsis in the center to shorten.
+--
+-- The total length will be `l + r + 3` characters.
+--
+-- >>> format (cfixed 4 3 ' ' int) 123
+-- "    123   "
+--
+-- >>> format (cfixed 4 3 ' ' int) 1234567890
+-- "1234567890"
+--
+-- >>> format (cfixed 4 3 ' ' int) 123456789012345
+-- "1234...345"
+cfixed :: Int64 -> Int64 -> Char -> Format r (a -> r) -> Format r (a -> r)
+cfixed l r c = ctruncated l r . cpadded (l + r + 3) c
+
+-- | Add the given prefix to the formatted item:
+--
+-- >>> format ("The answer is: " % prefixed "wait for it... " int) 42
+-- "The answer is: wait for it... 42"
+--
+-- >>> fprint (unlined (indented 4 (prefixed "- " int))) [1, 2, 3]
+--     - 1
+--     - 2
+--     - 3
+prefixed :: Builder -> Format r a -> Format r a
+prefixed s f = now s % f
+
+-- | Add the given suffix to the formatted item.
+suffixed :: Builder -> Format r a -> Format r a
+suffixed s f = f % now s
+
+-- | Surround the output string with the given string:
+--
+-- >>> format (surrounded "***" string) "glue"
+-- "***glue***"
+surrounded :: Builder -> Format r a -> Format r a
+surrounded s f = now s % f % now s
+
+-- | Enclose the output string with the given strings:
+--
+-- >>> format (enclosed "<!--" "-->" text) "an html comment"
+-- "<!--an html comment-->"
+enclosed :: Builder -> Builder -> Format r a -> Format r a
+enclosed pre suf f = now pre % f % now suf
+
+-- | Add single quotes around the formatted item:
+--
+-- >>> let obj = Just Nothing in format ("The object is: " % squoted shown % ".") obj
+-- "The object is: 'Just Nothing'."
+squoted :: Format r a -> Format r a
+squoted = surrounded "'"
+
+-- | Add double quotes around the formatted item:
+--
+-- >>> fprintLn ("He said it was based on " % dquoted stext % ".") "science"
+-- He said it was based on "science".
+dquoted :: Format r a -> Format r a
+dquoted = surrounded "\""
+
+-- | Add parentheses around the formatted item:
+--
+-- >>> format ("We found " % parenthesised int % " discrepancies.") 17
+-- "We found (17) discrepancies."
+--
+-- >>> fprintLn (took 5 (list (parenthesised int))) [1..]
+-- [(1), (2), (3), (4), (5)]
+parenthesised :: Format r a -> Format r a
+parenthesised = enclosed "(" ")"
+
+-- | Add square brackets around the formatted item:
+--
+-- >>> format (squared int) 7
+-- "[7]"
+squared :: Format r a -> Format r a
+squared = enclosed "[" "]"
+
+-- | Add curly brackets around the formatted item:
+--
+-- >>> format ("\\begin" % braced text) "section"
+-- "\\begin{section}"
+braced :: Format r a -> Format r a
+braced = enclosed "{" "}"
+
+-- | Add angle brackets around the formatted item:
+--
+-- >>> format (angled int) 7
+-- "<7>"
+--
+-- >>> format (list (angled text)) ["html", "head", "title", "body", "div", "span"]
+-- "[<html>, <head>, <title>, <body>, <div>, <span>]"
+angled :: Format r a -> Format r a
+angled = enclosed "<" ">"
+
+-- | Add backticks around the formatted item:
+--
+-- >>> format ("Be sure to run " % backticked builder % " as root.") ":(){:|:&};:"
+-- "Be sure to run `:(){:|:&};:` as root."
+backticked :: Format r a -> Format r a
+backticked = surrounded "`"
+
+-- | Insert the given number of spaces at the start of the rendered text:
+--
+-- >>> format (indented 4 int) 7
+-- "    7"
+--
+-- Note that this only indents the first line of a multi-line string.
+-- To indent all lines see 'reindented'.
+indented :: Int -> Format r a -> Format r a
+indented n = prefixed spaces
+  where
+    spaces = TL.replicate (fromIntegral n) (TL.singleton ' ') & TLB.fromLazyText
+
+-- | Format a list of items, placing one per line, indented by the given number of spaces.
+--
+-- >>> fprintLn ("The lucky numbers are:\n" % indentedLines 4 int) [7, 13, 1, 42]
+-- The lucky numbers are:
+--     7
+--     13
+--     1
+--     42
+indentedLines :: Foldable t => Int -> Format Builder (a -> Builder) -> Format r (t a -> r)
+indentedLines n = unlined . indented n
+
+-- | Indent each line of the formatted string by the given number of spaces:
+--
+-- >>> fprint (reindented 2 text) "one\ntwo\nthree"
+--   one
+--   two
+--   three
+reindented :: Int -> Format r a -> Format r a
+reindented n = lined (indentedLines n)
+
+-- | Take a fractional number and round it before formatting it as the given Format:
+--
+-- >>> format (roundedTo int) 6.66
+-- "7"
+-- >>> format (list (roundedTo int)) [10.66, 6.66, 1.0, 3.4]
+-- "[11, 7, 1, 3]"
+--
+-- Note: the type variable 'f' will almost always be 'Format r', so the type of this function can be thought of as:
+--
+-- @
+-- roundedTo :: (Integral i, RealFrac d) => Format r (i -> r) -> Format r (d -> r)
+-- @
+roundedTo :: (Integral i, RealFrac d, Functor f) => f (i -> r) -> f (d -> r)
+roundedTo = fmap (. round)
+
+-- | Take a fractional number and truncate it before formatting it as the given Format:
+--
+-- >>> format (truncatedTo int) 6.66
+-- "6"
+-- >>> format (list (truncatedTo int)) [10.66, 6.66, 1.0, 3.4]
+-- "[10, 6, 1, 3]"
+--
+-- Note: the type variable 'f' will almost always be 'Format r', so the type of this function can be thought of as:
+--
+-- @
+-- truncatedTo :: (Integral i, RealFrac d) => Format r (i -> r) -> Format r (d -> r)
+-- @
+truncatedTo :: (Integral i, RealFrac d, Functor f) => f (i -> r) -> f (d -> r)
+truncatedTo = fmap (. truncate)
+
+-- | Take a fractional number and ceiling it before formatting it as the given Format:
+--
+-- >>> format (ceilingedTo int) 6.66
+-- "7"
+-- >>> format (list (ceilingedTo int)) [10.66, 6.66, 1.0, 3.4]
+-- "[11, 7, 1, 4]"
+--
+-- Note: the type variable 'f' will almost always be 'Format r', so the type of this function can be thought of as:
+--
+-- @
+-- ceilingedTo :: (Integral i, RealFrac d) => Format r (i -> r) -> Format r (d -> r)
+-- @
+ceilingedTo :: (Integral i, RealFrac d, Functor f) => f (i -> r) -> f (d -> r)
+ceilingedTo = fmap (. ceiling)
+
+-- | Take a fractional number and floor it before formatting it as the given Format:
+--
+-- >>> format (flooredTo int) 6.66
+-- "6"
+-- >>> format (list (flooredTo int)) [10.66, 6.66, 1.0, 3.4]
+-- "[10, 6, 1, 3]"
+--
+-- Note: the type variable 'f' will almost always be 'Format r', so the type of this function can be thought of as:
+--
+-- @
+-- flooredTo :: (Integral i, RealFrac d) => Format r (i -> r) -> Format r (d -> r)
+-- @
+flooredTo :: (Integral i, RealFrac d, Functor f) => f (i -> r) -> f (d -> r)
+flooredTo = fmap (. floor)
+
+-- | Use the given lens to view an item, formatting it with the given formatter.
+--
+-- You can think of this as having the type:
+--
+-- @
+-- 'viewed' :: 'Lens'' s a -> Format r (a -> r) -> Format r (s -> r)
+-- @
+--
+-- >>> format (viewed _1 int) (1, "hello")
+-- "1"
+--
+-- This is useful when combined with the Monoid instance for Format, because it allows us to give a data structure as an argument only once, and deconstruct it with the formatters:
+--
+-- @
+-- data Person = Person
+--   { _personName :: Text
+--   , _personAge :: Int
+--   }
+-- makeLenses ''Person
+--
+-- me :: Person
+-- me = Person "Alex" 38
+--
+-- format ("The person's name is " % squoted (viewed personName text) % ", and their age is " <> viewed personAge int) me
+-- "The person's name is 'Alex', and their age is 38"
+-- @
+viewed :: ((a -> Const a b) -> s -> Const a t) -> Format r (a -> r) -> Format r (s -> r)
+viewed l = fmap (. (getConst . l Const))
+
+-- | Access an element of the structure and format it with the given formatter.
+--
+-- >>> format (accessed fst int) (1, "hello")
+-- "1"
+--
+-- Repeating the example from 'viewed':
+--
+-- format ("The person's name is " % squoted (accessed _personName text) % ", and their age is " <> accessed _personAge int) me
+-- "The person's name is 'Alex', and their age is 38"
+accessed :: (s -> a) -> Format r (a -> r) -> Format r (s -> r)
+accessed accessor = fmap (. accessor)
+
+-- | Render an integer using binary notation with a leading 0b, padding with zeroes to the given width:
+--
+-- >>> format (binPrefix 16) 4097
+-- "0b0001000000000001"
+binPrefix :: Integral a => Int64 -> Format r (a -> r)
+binPrefix n = "0b" % lpadded n '0' bin
+
+-- | Render an integer using octal notation with a leading 0o, padding with zeroes to the given width:
+--
+-- >>> format (octPrefix 16) 4097
+-- "0o0000000000010001"
+octPrefix :: Integral a => Int64 -> Format r (a -> r)
+octPrefix n = "0o" % lpadded n '0' oct
+
+-- | Render an integer using octal notation with a leading 0x, padding with zeroes to the given width:
+--
+-- >>> format (hexPrefix 16) 4097
+-- "0x0000000000001001"
+hexPrefix :: Integral a => Int64 -> Format r (a -> r)
+hexPrefix n = "0x" % lpadded n '0' hex
diff --git a/src/Formatting/Examples.hs b/src/Formatting/Examples.hs
--- a/src/Formatting/Examples.hs
+++ b/src/Formatting/Examples.hs
@@ -3,7 +3,16 @@
 -- | Examples that should always compile. If reading on Haddock, you
 -- can view the sources to each of these.
 
-module Formatting.Examples where
+module Formatting.Examples
+  ( hello
+  , strings
+  , texts
+  , builders
+  , integers
+  , floats
+  , hexes
+  , padding
+  ) where
 
 import Data.Text.Lazy (Text)
 import Data.Text.Lazy.Builder (Builder)
@@ -11,7 +20,7 @@
 
 -- | Simple hello, world!
 hello :: Text
-hello = format ("Hello, World!")
+hello = format "Hello, World!"
 
 -- | Printing strings.
 strings :: Text
diff --git a/src/Formatting/Formatters.hs b/src/Formatting/Formatters.hs
--- a/src/Formatting/Formatters.hs
+++ b/src/Formatting/Formatters.hs
@@ -1,12 +1,12 @@
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS -Wall #-}
+{-# LANGUAGE CPP #-}
 
 -- |
 -- Module      : Formatting.Formatters
 -- Copyright   : (c) 2013 Chris Done, 2013 Shachaf Ben-Kiki
 -- License     : BSD3
--- Maintainer  : chrisdone@gmail.com
+-- Maintainer  : alex@farfromthere.net
 -- Stability   : experimental
 -- Portability : GHC
 --
@@ -57,7 +57,9 @@
 import           Formatting.Internal
 
 import           Data.Char (chr, ord)
+#if !(MIN_VERSION_base(4,11,0))
 import           Data.Monoid ((<>))
+#endif
 import           Data.Scientific
 import qualified Data.Text as S
 import qualified Data.Text as T
@@ -113,7 +115,7 @@
 
 -- | Render some floating point with the usual notation, e.g. 123.32 => \"123.32\"
 float :: Real a => Format r (a -> r)
-float = later (T.shortest)
+float = later T.shortest
 
 -- | Render a floating point number using normal notation, with the
 -- given number of decimal places.
@@ -121,7 +123,8 @@
 fixed i = later (T.fixed i)
 
 -- | Render a floating point number using the smallest number of
--- digits that correctly represent it.
+-- digits that correctly represent it. Note that in the case of whole
+-- numbers it will still add one decimal place, e.g. "1.0".
 shortest :: Real a => Format r (a -> r)
 shortest = later T.shortest
 
@@ -210,6 +213,15 @@
           where tens = n `mod` 100
 
 -- | English plural suffix for an integral.
+--
+-- For example:
+--
+-- >>> set -XOverloadedStrings
+-- >>> formatPeople = format (int % " " <> plural "person" "people" % ".") :: Int -> Data.Text.Lazy.Text
+-- >>> formatPeople 1
+-- "1 person."
+-- >>> formatPeople 3
+-- "3 people."
 plural :: (Num a, Eq a) => Text -> Text -> Format r (a -> r)
 plural s p = later (\i -> if i == 1 then B.build s else B.build p)
 
@@ -236,16 +248,22 @@
 {-# INLINE hex #-}
 
 -- | Render an integer using binary notation with a leading 0b.
+--
+-- See also 'Formatting.Combinators.binPrefix' for fixed-width formatting.
 prefixBin :: Integral a => Format r (a -> r)
 prefixBin = "0b" % bin
 {-# INLINE prefixBin #-}
 
 -- | Render an integer using octal notation with a leading 0o.
+--
+-- See also 'Formatting.Combinators.octPrefix' for fixed-width formatting.
 prefixOct :: Integral a => Format r (a -> r)
 prefixOct = "0o" % oct
 {-# INLINE prefixOct #-}
 
 -- | Render an integer using hexadecimal notation with a leading 0x.
+--
+-- See also 'Formatting.Combinators.hexPrefix' for fixed-width formatting.
 prefixHex :: Integral a => Format r (a -> r)
 prefixHex = "0x" % hex
 {-# INLINE prefixHex #-}
diff --git a/src/Formatting/Internal.hs b/src/Formatting/Internal.hs
--- a/src/Formatting/Internal.hs
+++ b/src/Formatting/Internal.hs
@@ -1,9 +1,27 @@
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE CPP #-}
 
 -- | Internal format starters.
 
-module Formatting.Internal where
+module Formatting.Internal
+  ( Format(..)
+  , (%)
+  , (%.)
+  , now
+  , bind
+  , mapf
+  , later
+  , format
+  , sformat
+  , bprint
+  , bformat
+  , fprint
+  , fprintLn
+  , hprint
+  , hprintLn
+  , formatToString
+  ) where
 
 import           Control.Category (Category(..))
 import           Data.Monoid
@@ -26,7 +44,7 @@
 -- will be formatted.  For example, in
 --
 -- @
--- myFormat :: Formatter r (Text -> Int -> r)
+-- myFormat :: Format r (Text -> Int -> r)
 -- myFormat = \"Person's name is \" % text % \", age is \" % hex
 -- @
 --
@@ -44,21 +62,38 @@
 newtype Format r a =
   Format {runFormat :: (Builder -> r) -> a}
 
--- | Not particularly useful, but could be.
+-- | This can be used almost like contramap, e.g:
+--
+-- @
+-- formatter :: Format r (b -> r)
+-- formatter = _
+-- 
+-- adapter :: a -> b
+-- adapter = _
+-- 
+-- adapted :: Format r (a -> r)
+-- adapted = fmap (. adapter) formatter
+-- @
 instance Functor (Format r) where
-  fmap f (Format k) = Format (\br -> f (k br))
+  fmap f (Format k) = Format (f . k)
 
--- | Useful instance for applying two formatters to the same input
--- argument. For example: @format (year <> "/" % month) now@ will
--- yield @"2015/01"@.
-instance Monoid (Format r (a -> r)) where
-  mappend m n =
+instance Data.Semigroup.Semigroup (Format r (a -> r)) where
+  m <> n =
     Format (\k a ->
               runFormat m (\b1 -> runFormat n (\b2 -> k (b1 <> b2)) a) a)
-  mempty = Format (\k _ -> k mempty)
 
-instance Data.Semigroup.Semigroup (Format r (a -> r)) where
-  (<>) = mappend
+-- | Useful instance for applying two formatters to the same input
+-- argument. For example: @format (year <> "/" % month) now@ will
+-- yield @"2015/01"@.
+instance
+#if !(MIN_VERSION_base(4,11,0))
+  Semigroup (Format r (a -> r)) =>
+#endif
+  Monoid (Format r (a -> r)) where
+#if !(MIN_VERSION_base(4,11,0))
+    mappend = (<>)
+#endif
+    mempty = Format (\k _ -> k mempty)
 
 -- | Useful instance for writing format string. With this you can
 -- write @"Foo"@ instead of @now "Foo!"@.
@@ -96,14 +131,14 @@
 -- @
 --
 -- @
--- myFormat :: Formatter r (Text -> Int -> r)
+-- myFormat :: Format r (Text -> Int -> r)
 -- myFormat = format1 % format2 % format3
 -- @
 --
 -- Notice how the argument types of @format1@ and @format3@ are
 -- gathered into the type of @myFormat@.
 --
--- (This is actually the composition operator for 'Format''s
+-- (This is actually the composition operator for 'Format's
 -- 'Category' instance, but that is (at present) inconvenient to use
 -- with regular "Prelude". So this function is provided as a
 -- convenience.)
@@ -147,14 +182,28 @@
 bprint :: Format Builder a -> a
 bprint m = runFormat m id
 
+-- | Run the formatter and return a 'Builder' value.
+-- 
+-- This is a newer synonym for 'bprint', following the naming convention set by 'format' and 'sformat'.
+bformat :: Format Builder a -> a
+bformat m = runFormat m id
+
 -- | Run the formatter and print out the text to stdout.
 fprint :: Format (IO ()) a -> a
 fprint m = runFormat m (T.putStr . T.toLazyText)
 
+-- | Run the formatter and print out the text to stdout, followed by a newline.
+fprintLn :: Format (IO ()) a -> a
+fprintLn m = runFormat m (T.putStrLn . T.toLazyText)
+
 -- | Run the formatter and put the output onto the given 'Handle'.
 hprint :: Handle -> Format (IO ()) a -> a
 hprint h m = runFormat m (T.hPutStr h . T.toLazyText)
 
+-- | Run the formatter and put the output and a newline onto the given 'Handle'.
+hprintLn :: Handle -> Format (IO ()) a -> a
+hprintLn h m = runFormat m (T.hPutStrLn h . T.toLazyText)
+
 -- | Run the formatter and return a list of characters.
-formatToString :: Format [Char] a -> a
+formatToString :: Format String a -> a
 formatToString m = runFormat m (TL.unpack . TLB.toLazyText)
diff --git a/src/Formatting/ShortFormatters.hs b/src/Formatting/ShortFormatters.hs
--- a/src/Formatting/ShortFormatters.hs
+++ b/src/Formatting/ShortFormatters.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE OverloadedStrings #-}
 {-# OPTIONS -Wall #-}
 
 -- |
@@ -12,7 +11,21 @@
 --
 -- Single letters for short formatting.
 
-module Formatting.ShortFormatters where
+module Formatting.ShortFormatters
+  ( t
+  , d
+  , b
+  , o
+  , x
+  , st
+  , s
+  , sh
+  , c
+  , f
+  , sf
+  , l
+  , r
+  ) where
 
 import           Formatting.Formatters (bin, int, oct)
 import           Formatting.Internal
diff --git a/src/Formatting/Time.hs b/src/Formatting/Time.hs
--- a/src/Formatting/Time.hs
+++ b/src/Formatting/Time.hs
@@ -4,22 +4,75 @@
 
 -- | Formatters for time.
 
-module Formatting.Time where
+module Formatting.Time
+  ( tz
+  , tzName
+  , datetime
+  , hm
+  , hms
+  , hmsL
+  , hmsPL
+  , dayHalf
+  , dayHalfU
+  , hour24
+  , hour12
+  , hour24S
+  , hour12S
+  , minute
+  , second
+  , pico
+  , decimals
+  , epoch
+  , dateSlash
+  , dateDash
+  , dateSlashL
+  , year
+  , yy
+  , century
+  , monthName
+  , monthNameShort
+  , month
+  , dayOfMonth
+  , dayOfMonthOrd
+  , dayOfMonthS
+  , day
+  , weekYear
+  , weekYY
+  , weekCentury
+  , week
+  , dayOfWeek
+  , dayNameShort
+  , dayName
+  , weekFromZero
+  , dayOfWeekFromZero
+  , weekOfYearMon
+  , diff
+  , years
+  , days
+  , hours
+  , minutes
+  , seconds
+  , diffComponents
+  , customDiffComponents
+  , fmt
+  , customTimeFmt
+  ) where
 
 import           Data.List
-import           Data.Text.Lazy.Builder
-import           Formatting.Formatters  hiding (build)
+import           Data.Tuple
+import           Formatting.Formatters  hiding (build, base)
 import           Formatting.Internal
 
 import           Data.Text              (Text)
 import qualified Data.Text              as T
 import           Formatting.Buildable
-import           Data.Time
+import           Data.Time (FormatTime, formatTime, defaultTimeLocale)
 #if MIN_VERSION_time(1,5,0)
-import           System.Locale hiding (defaultTimeLocale)
+import           System.Locale ()
 #else
 import           System.Locale
 #endif
+import           Control.Monad.Trans.State.Strict
 
 -- * For 'TimeZone' (and 'ZonedTime' and 'UTCTime'):
 
@@ -230,63 +283,79 @@
     ranges =
       [(0,int % " milliseconds",0.001)
       ,(1,int % " seconds",1)
-      ,(minute,fconst "a minute",0)
-      ,(minute*2,int % " minutes",minute)
-      ,(minute*30,fconst "half an hour",0)
-      ,(minute*31,int % " minutes",minute)
-      ,(hour,fconst "an hour",0)
-      ,(hour*2,int % " hours",hour)
-      ,(hour*3,fconst "a few hours",0)
-      ,(hour*4,int % " hours",hour)
-      ,(day,fconst "a day",0)
-      ,(day*2,int % " days",day)
-      ,(week,fconst "a week",0)
-      ,(week*2,int % " weeks",week)
-      ,(month,fconst "a month",0)
-      ,(month*2,int % " months",month)
-      ,(year,fconst "a year",0)
-      ,(year*2,int % " years",year)]
-      where year = month * 12
-            month = day * 30
-            week = day * 7
-            day = hour * 24
-            hour = minute * 60
-            minute = 60
+      ,(minute',fconst "a minute",0)
+      ,(minute'*2,int % " minutes",minute')
+      ,(minute'*30,fconst "half an hour",0)
+      ,(minute'*31,int % " minutes",minute')
+      ,(hour',fconst "an hour",0)
+      ,(hour'*2,int % " hours",hour')
+      ,(hour'*3,fconst "a few hours",0)
+      ,(hour'*4,int % " hours",hour')
+      ,(day',fconst "a day",0)
+      ,(day'*2,int % " days",day')
+      ,(week',fconst "a week",0)
+      ,(week'*2,int % " weeks",week')
+      ,(month',fconst "a month",0)
+      ,(month'*2,int % " months",month')
+      ,(year',fconst "a year",0)
+      ,(year'*2,int % " years",year')]
+      where year' = month' * 12
+            month' = day' * 30
+            week' = day' * 7
+            day' = hour' * 24
+            hour' = minute' * 60
+            minute' = 60
 
 -- | Display the absolute value time span in years.
 years :: (RealFrac n)
       => Int -- ^ Decimal places.
       -> Format r (n -> r)
 years n = later (bprint (fixed n) . abs . count)
-  where count n = n / 365 / 24 / 60 / 60
+  where count n' = n' / 365 / 24 / 60 / 60
 
 -- | Display the absolute value time span in days.
 days :: (RealFrac n)
       => Int -- ^ Decimal places.
       -> Format r (n -> r)
 days n = later (bprint (fixed n) . abs . count)
-  where count n = n / 24 / 60 / 60
+  where count n' = n' / 24 / 60 / 60
 
 -- | Display the absolute value time span in hours.
 hours :: (RealFrac n)
       => Int -- ^ Decimal places.
       -> Format r (n -> r)
 hours n = later (bprint (fixed n) . abs . count)
-  where count n = n / 60 / 60
+  where count n' = n' / 60 / 60
 
 -- | Display the absolute value time span in minutes.
 minutes :: (RealFrac n)
       => Int -- ^ Decimal places.
       -> Format r (n -> r)
 minutes n = later (bprint (fixed n) . abs . count)
-  where count n = n / 60
+  where count n' = n' / 60
 
 -- | Display the absolute value time span in seconds.
 seconds :: (RealFrac n)
       => Int -- ^ Decimal places.
       -> Format r (n -> r)
 seconds n = later (bprint (fixed n) . abs . count)
-  where count n = n
+  where count n' = n'
+
+-- | Display seconds in the following pattern:
+-- @00:00:00:00@, which ranges from days to seconds.
+diffComponents :: (RealFrac n) => Format r (n -> r)
+diffComponents = customDiffComponents (left 2 '0' % ":" % left 2 '0' % ":" % left 2 '0' % ":" % left 2 '0')
+
+-- | Variation of 'diffComponents',
+-- which lets you explicitly specify how to render each component.
+customDiffComponents :: (RealFrac n) => (forall r'. Format r' (Integer -> Integer -> Integer -> Integer -> r')) -> Format r (n -> r)
+customDiffComponents subFormat = later builder' where
+  builder' diffTime = flip evalState (round diffTime) $ do
+    seconds' <- state (swap . flip divMod 60)
+    minutes' <- state (swap . flip divMod 60)
+    hours' <- state (swap . flip divMod 24)
+    days' <- get
+    return (bprint subFormat days' hours' minutes' seconds')
 
 -- * Internal.
 
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,11 +1,15 @@
 {-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS -Wno-type-defaults #-}
 
 import Control.Monad
+import Data.Char (isSpace)
 import Data.Int
 import qualified Data.Monoid
+import Data.Scientific
 import qualified Data.Semigroup
 import qualified Data.Text.Lazy as LT
 import Formatting as F
+import Formatting.Time
 import Test.Hspec
 
 main :: IO ()
@@ -13,56 +17,49 @@
 
 spec :: Spec
 spec = do
-  describe
-    "Regression tests"
-    (do describe "https://github.com/chrisdone/formatting/issues/36"
-                 (do it "format (later id <> later id) \"x\""
-                        (shouldBe (format (later id Data.Monoid.<> later id) "x")
-                                  "xx")
-                     it "format (later id <> later id) \"x\""
-                        (shouldBe (format (later id Data.Semigroup.<> later id) "x")
-                                  "xx"))
-        describe
-          "https://github.com/chrisdone/formatting/issues/31"
-          (do it
-                "10^6-1"
-                (shouldBe
-                   (F.format F.int (10 ^ (16 :: Int) - 1 :: Int))
-                   "9999999999999999"))
-        describe
-          "https://github.com/chrisdone/formatting/issues/28"
-          (do it
-                "-100"
-                (shouldBe (sformat (groupInt 3 ',') (-100 :: Int)) "-100")
-              it
-                "-100,000,000"
-                (shouldBe
-                   (sformat (groupInt 3 ',') (-100000000 :: Int))
-                   "-100,000,000")
-              it
-                "100,000,000"
-                (shouldBe
-                   (sformat (groupInt 3 ',') (-100000000 :: Int))
-                   "-100,000,000"))
-        describe
-          "https://github.com/bos/text-format/issues/18"
-          (do it
-                "build (minBound :: Int)"
-                (shouldBe
-                   (format build (minBound :: Int64))
-                   "-9223372036854775808"))
-        it
-          "build (maxBound :: Int)"
-          (shouldBe
-             (format build (maxBound :: Int))
-             "9223372036854775807"))
-  describe
-    "Floating point"
-    (do it "Fixed" (shouldBe (format (fixed 4) (12.123456 :: Double)) "12.1235")
-        it
-          "Variable"
-          (shouldBe (format float (12.123456 :: Double)) "12.123456"))
+  describe "Regression tests" $ do
+    describe "https://github.com/AJChapman/formatting/issues/36" $ do
+      it "format (later id <> later id) \"x\"" $ format (later id Data.Monoid.<> later id)    "x" `shouldBe` "xx"
+      it "format (later id <> later id) \"x\"" $ format (later id Data.Semigroup.<> later id) "x" `shouldBe` "xx"
 
+    describe "https://github.com/AJChapman/formatting/issues/31" $
+      it "10^6-1" $ F.format F.int (10 ^ (16 :: Int) - 1 :: Int) `shouldBe` "9999999999999999"
+
+    describe "https://github.com/AJChapman/formatting/issues/28" $ do
+      it "-100"         $ sformat (groupInt 3 ',') (-100 :: Int)       `shouldBe` "-100"
+      it "-100,000,000" $ sformat (groupInt 3 ',') (-100000000 :: Int) `shouldBe` "-100,000,000"
+      it "100,000,000"  $ sformat (groupInt 3 ',') (-100000000 :: Int) `shouldBe` "-100,000,000"
+
+    describe "https://github.com/bos/text-format/issues/18" $ do
+      it "build (minBound :: Int)" $ format build (minBound :: Int64) `shouldBe` "-9223372036854775808"
+      it "build (maxBound :: Int)" $ format build (maxBound :: Int)   `shouldBe` "9223372036854775807"
+
+    describe "https://github.com/AJChapman/formatting/issues/62" $ do
+      it "left 3 '0' (0 :: Int)"  $ format (left 3 '0') (0 ::Int)  `shouldBe` "000"
+      it "left 3 '0' (0 :: Word)" $ format (left 3 '0') (0 ::Word) `shouldBe` "000"
+
+    describe "https://github.com/AJChapman/formatting/issues/60" $
+      it "build (minBound :: Word)" $ format build (minBound :: Word) `shouldBe` "0"
+
+    describe "https://github.com/AJChapman/formatting/issues/59" $
+      it "shortest not scientific" $ format shortest (0.01 :: Double) `shouldBe` "0.01"
+
+  describe "Floating point" $ do
+    it "Fixed"    $ format (fixed 4) (12.123456 :: Double) `shouldBe` "12.1235"
+    it "Variable" $ format float     (12.123456 :: Double) `shouldBe` "12.123456"
+    it "Shortest" $ format shortest  (12.0000 :: Double) `shouldBe` "12"
+
+  describe "Scientific" $ do
+    it "sci" $ format sci (scientific 60221409 16) `shouldBe` "6.0221409e23"
+    it "scifmt" $ format (scifmt Exponent (Just 3)) (scientific 60221409 16) `shouldBe` "6.022e23"
+    it "scifmt" $ format (scifmt Exponent Nothing) (scientific 60221409 16) `shouldBe` "6.0221409e23"
+    it "scifmt" $ format (scifmt Fixed Nothing) (scientific 60221409 16) `shouldBe` "602214090000000000000000.0"
+    it "scifmt" $ format (scifmt Generic (Just 5)) (scientific 60221409 16) `shouldBe` "6.02214e23"
+
+  describe "Bytes" $ do
+    it "1KB" $ format (bytes shortest) (1024 :: Int) `shouldBe` "1KB"
+    it "1.15GB" $ format (bytes (fixed 2)) (1234567890 :: Int) `shouldBe` "1.15GB"
+
   describe
     "Buildable a => Buildable [a]"
     (do it "\"\" :: [Char] (backwards compatibility)"
@@ -74,7 +71,7 @@
         it "[] :: [Int]"
            (shouldBe (format build ([] :: [Int])) "[]"))
 
-  describe "ords" $ do 
+  describe "ords" $ do
       let tests :: [(Int, String)]
           tests = [ ( 1, "1st")
                   , ( 2, "2nd")
@@ -113,5 +110,98 @@
                   , (34, "34th")
                   ]
 
-      forM_ tests $ \(input, output) -> it output $ format ords input `shouldBe` (LT.pack output)
-    
+      forM_ tests $ \(input, output) -> it output $ format ords input `shouldBe` LT.pack output
+
+  describe "plural" $ do
+    let formatPeople = format (int % " " <> plural "person" "people" % ".")
+    it "formats a person" $ formatPeople (1 :: Int) `shouldBe` "1 person."
+    it "formats a person" $ formatPeople (3 :: Int) `shouldBe` "3 people."
+
+  describe "diffComponents" $ do
+    it "59s" $ flip shouldBe "00:00:00:59" $ format diffComponents (59 :: Double)
+    it "minute" $ flip shouldBe "00:00:01:00" $ format diffComponents (60 :: Double)
+    it "90s" $ flip shouldBe "00:00:01:30" $ format diffComponents (90 :: Double)
+    it "hour" $ flip shouldBe "00:01:00:00" $ format diffComponents (3600 :: Double)
+    it "day" $ flip shouldBe "01:00:00:00" $ format diffComponents (86400 :: Double)
+
+  describe "list formatters" $ do
+    it "concatenated" $ format (concatenated text) ["one", "two", "three"] `shouldBe` "onetwothree"
+    it "joinedWith" $ format (joinedWith (mconcat . reverse) int) [123, 456, 789] `shouldBe` "789456123"
+    it "intercalated" $ format (intercalated "||" int) [1, 2, 3]  `shouldBe` "1||2||3"
+    it "unworded" $ format (unworded int) [1, 2, 3] `shouldBe` "1 2 3"
+    it "unlined" $ format (unlined char) ['a'..'c'] `shouldBe` "a\nb\nc\n"
+    it "spaced" $ format (spaced int) [1, 2, 3] `shouldBe` "1 2 3"
+    it "commaSep" $ format (took 5 (commaSep int)) [1..] `shouldBe` "1,2,3,4,5"
+    it "commaSpaceSep" $ format (took 3 (commaSpaceSep ords)) [1..] `shouldBe` "1st, 2nd, 3rd"
+    it "list" $ format (list stext) ["one", "two", "three"] `shouldBe` "[one, two, three]"
+    it "qlist" $ format (qlist stext) ["one", "two", "three"] `shouldBe` "[\"one\", \"two\", \"three\"]"
+    it "took" $ format (took 7 (list bin)) [1..] `shouldBe` "[1, 10, 11, 100, 101, 110, 111]"
+    it "dropped" $ format (dropped 3 (list int)) [1..6] `shouldBe` "[4, 5, 6]"
+
+  describe "splitting formatters" $ do
+    it "splat" $ format (splat isSpace commaSpaceSep stext) "This\t  is\n\t\t  poorly formatted   " `shouldBe` "This, , , is, , , , , poorly, formatted, , , "
+    it "splatWith" $ format (splatWith (LT.chunksOf 3) list int) 1234567890 `shouldBe` "[123, 456, 789, 0]"
+    it "splatOn" $ format (splatOn "," unlined text) "one,two,three" `shouldBe` "one\ntwo\nthree\n"
+    it "worded" $ format (worded list text) "one  two three  " `shouldBe` "[one, two, three]"
+    it "lined" $ format (lined qlist text) "one two three\n\nfour five six\nseven eight nine\n\n" `shouldBe` "[\"one two three\", \"\", \"four five six\", \"seven eight nine\", \"\"]"
+
+  describe "altering combinators" $ do
+    it "alteredWith" $ format (alteredWith LT.reverse int) 123456 `shouldBe` "654321"
+    it "replaced" $ format (replaced "Bruce" "<redacted>" stext) "Bruce replied that Bruce's name was, in fact, '<redacted>'." `shouldBe` "<redacted> replied that <redacted>'s name was, in fact, '<redacted>'."
+    it "uppercased" $ format (uppercased text) "I'm not shouting, you're shouting." `shouldBe` "I'M NOT SHOUTING, YOU'RE SHOUTING."
+    it "lowercased" $ format (lowercased text) "Cd SrC/; Rm -Rf *" `shouldBe` "cd src/; rm -rf *"
+    it "titlecased" $ format (titlecased string) "the life of brian" `shouldBe` "The Life Of Brian"
+    it "ltruncated" $ format (ltruncated 5 text) "hellos" `shouldBe` "he..."
+    it "ltruncated, non-truncated" $ format (ltruncated 5 text) "hello" `shouldBe` "hello"
+    it "rtruncated" $ format (rtruncated 5 text) "hellos" `shouldBe` "...os"
+    it "rtruncated, non-truncated" $ format (rtruncated 5 text) "hello" `shouldBe` "hello"
+    it "ctruncated" $ format (ctruncated 15 4 text) "The quick brown fox jumps over the lazy dog." `shouldBe` "The quick brown...dog."
+    it "ctruncated, non-truncated" $ format (ctruncated 15 4 text) "The quick brown fox" `shouldBe` "The quick brown fox"
+    it "lpadded" $ format (lpadded 7 ' ' int) 1 `shouldBe` "      1"
+    it "lpadded doesn't shorten" $ format (lpadded 7 ' ' int) 123456789 `shouldBe` "123456789"
+    it "rpadded" $ format (rpadded 7 ' ' int) 1 `shouldBe` "1      "
+    it "cpadded" $ format (cpadded 7 ' ' int) 1 `shouldBe` "   1   "
+    it "lfixed short" $ format (lfixed 10 ' ' int) 123 `shouldBe` "123       "
+    it "lfixed at length" $ format (lfixed 10 ' ' int) 1234567890 `shouldBe` "1234567890"
+    it "lfixed long" $ format (lfixed 10 ' ' int) 123456789012345 `shouldBe` "1234567..."
+    it "rfixed short" $ format (rfixed 10 ' ' int) 123 `shouldBe` "       123"
+    it "rfixed at length" $ format (rfixed 10 ' ' int) 1234567890 `shouldBe` "1234567890"
+    it "rfixed long" $ format (rfixed 10 ' ' int) 123456789012345 `shouldBe` "...9012345"
+    it "cfixed short" $ format (cfixed 4 3 ' ' int) 123 `shouldBe` "    123   "
+    it "cfixed at length" $ format (cfixed 4 3 ' ' int) 1234567890 `shouldBe` "1234567890"
+    it "cfixed long" $ format (cfixed 4 3 ' ' int) 123456789012345 `shouldBe` "1234...345"
+
+  describe "wrapping combinators" $ do
+    it "prefixed" $ format ("The answer is: " % prefixed "wait for it... " int) 42 `shouldBe` "The answer is: wait for it... 42"
+    it "prefixed, combining" $ format (unlined (indented 4 (prefixed "- " int))) [1, 2, 3] `shouldBe` "    - 1\n    - 2\n    - 3\n"
+    it "suffixed" $ format (suffixed "!!!" int) 7 `shouldBe` "7!!!"
+    it "surrounded" $ format (surrounded "***" string) "glue" `shouldBe` "***glue***"
+    it "enclosed" $ format (enclosed "<!--" "-->" text) "an html comment" `shouldBe` "<!--an html comment-->"
+    it "squoted" $ let obj :: Maybe (Maybe Int); obj = Just Nothing in format ("The object is: " % squoted shown % ".") obj  `shouldBe` "The object is: 'Just Nothing'."
+    it "dquoted" $ format ("He said it was based on " % dquoted stext % ".") "science" `shouldBe` "He said it was based on \"science\"."
+    it "parenthesised" $ format (took 5 (list (parenthesised int))) [1..] `shouldBe` "[(1), (2), (3), (4), (5)]"
+    it "squared" $ format (squared int) 7 `shouldBe` "[7]"
+    it "braced" $ format ("\\begin" % braced text) "section" `shouldBe` "\\begin{section}"
+    it "angled" $ format (list (angled text)) ["html", "head", "title", "body", "div", "span"] `shouldBe` "[<html>, <head>, <title>, <body>, <div>, <span>]"
+    it "backticked" $ format ("Be sure to run " % backticked builder % " as root.") ":(){:|:&};:" `shouldBe` "Be sure to run `:(){:|:&};:` as root."
+
+  describe "indenters" $ do
+    it "indented" $ format (indented 4 int) 7 `shouldBe` "    7"
+    it "indentedLines" $ format ("The lucky numbers are:\n" % indentedLines 4 int) [7, 13, 1, 42] `shouldBe` "The lucky numbers are:\n    7\n    13\n    1\n    42\n"
+    it "reindented" $ format (reindented 2 text) "one\ntwo\nthree" `shouldBe` "  one\n  two\n  three\n"
+
+  describe "numerical adapters" $ do
+    it "roundedTo" $ format (list (roundedTo int)) [10.66, 6.66, 1.0, 3.4] `shouldBe` "[11, 7, 1, 3]"
+    it "truncatedTo" $ format (list (truncatedTo int)) [10.66, 6.66, 1.0, 3.4] `shouldBe` "[10, 6, 1, 3]"
+    it "ceilingedTo" $ format (list (ceilingedTo int)) [10.66, 6.66, 1.0, 3.4] `shouldBe` "[11, 7, 1, 4]"
+    it "flooredTo" $ format (list (flooredTo int)) [10.66, 6.66, 1.0, 3.4] `shouldBe` "[10, 6, 1, 3]"
+
+  describe "structure formatting" $
+    it "accessed" $ format (accessed fst int) (1, "hello") `shouldBe` "1"
+  -- describe "lens formatters" $ do
+  --   it "viewed" $ flip shouldBe "(viewed _1 int) (1, "hello")" $ format
+
+  describe "fixed-width numbers" $ do
+    it "binPrefix" $ format (binPrefix 16) 4097 `shouldBe` "0b0001000000000001"
+    it "octPrefix" $ format (octPrefix 16) 4097 `shouldBe` "0o0000000000010001"
+    it "hexPrefix" $ format (hexPrefix 16) 4097 `shouldBe` "0x0000000000001001"
