{- Acknowledgements ~~~~~~~~~~~~~~~~~~~ * 'prefixF', 'suffixF', 'padBothF', 'groupInt' are taken from Written by Github user @mwm * 'ordinalF' is taken from Written by Chris Done * 'atBase' is taken from , originally from Seems to be written by Johan Kiviniemi -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE CPP #-} #if __GLASGOW_HASKELL__ < 710 {-# LANGUAGE OverlappingInstances #-} # define _OVERLAPPING_ # define _OVERLAPPABLE_ # define _OVERLAPS_ #else # define _OVERLAPPING_ {-# OVERLAPPING #-} # define _OVERLAPPABLE_ {-# OVERLAPPABLE #-} # define _OVERLAPS_ {-# OVERLAPS #-} #endif {-# OPTIONS_GHC -fno-warn-orphans #-} module Fmt ( -- * Overloaded strings -- $overloadedstrings -- * Examples -- $examples -- * Migration guide from @formatting@ -- $migration -- * Basic formatting -- $brackets -- ** Ordinary brackets -- $god1 (+|), (|+), -- ** 'Show' brackets -- $god2 (+||), (||+), -- ** Combinations -- $god3 (|++|), (||++||), (|++||), (||++|), -- * Old-style formatting format, formatLn, TF.Format, -- * Helper functions fmt, fmtLn, Builder, Buildable(..), -- * Formatters indent, indent', nameF, -- ** Time module Fmt.Time, -- ** Lists listF, listF', blockListF, blockListF', jsonListF, jsonListF', -- ** Maps mapF, mapF', blockMapF, blockMapF', jsonMapF, jsonMapF', -- ** Tuples tupleF, tupleLikeF, -- ** ADTs maybeF, eitherF, -- ** Padding/trimming prefixF, suffixF, padLeftF, padRightF, padBothF, -- ** Hex hexF, -- ** Bytestrings base64F, base64UrlF, -- ** Integers ordinalF, commaizeF, -- *** Base conversion octF, binF, baseF, -- ** Floating-point floatF, exptF, precF, fixedF, -- ** Conditional formatting whenF, unlessF, -- ** Generic formatting genericF, ) where -- Generic useful things import Data.List import Data.Monoid import Lens.Micro -- Containers import Data.Map (Map) import qualified Data.Map as Map import Data.Set (Set) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet import Data.Sequence (Seq) -- Text import qualified Data.Text.Lazy as TL -- 'Buildable' and text-format import Data.Text.Buildable import qualified Data.Text.Format as TF -- Text 'Builder' import Data.Text.Lazy.Builder hiding (fromString) -- 'Foldable' and 'IsList' for list/map formatters import Data.Foldable (toList) #if __GLASGOW_HASKELL__ >= 708 import GHC.Exts (IsList, Item) import qualified GHC.Exts as IsList (toList) #endif -- Generics import GHC.Generics #if __GLASGOW_HASKELL__ < 710 import Data.Foldable (Foldable) #endif #if MIN_VERSION_base(4,9,0) import Data.List.NonEmpty (NonEmpty) #endif import Fmt.Internal import Fmt.Time {- $overloadedstrings You need @OverloadedStrings@ enabled to use this library. There are three ways to do it: * __In GHCi:__ do @:set -XOverloadedStrings@. * __In a module:__ add @\{\-\# LANGUAGE OverloadedStrings \#\-\}@ to the beginning of your module. * __In a project:__ add @OverloadedStrings@ to the @default-extensions@ section of your @.cabal@ file. -} {- $examples Here's a bunch of examples because some people learn better by looking at examples. Insert some variables into a string: >>> let (a, b, n) = ("foo", "bar", 25) >>> ("Here are some words: "+|a|+", "+|b|+"\nAlso a number: "+|n|+"") :: String "Here are some words: foo, bar\nAlso a number: 25" Print it: >>> fmtLn ("Here are some words: "+|a|+", "+|b|+"\nAlso a number: "+|n|+"") Here are some words: foo, bar Also a number: 25 Format a list in various ways: >>> let xs = ["John", "Bob"] >>> fmtLn ("Using show: "+||xs||+"\nUsing listF: "+|listF xs|+"") Using show: ["John","Bob"] Using listF: [John, Bob] >>> fmt ("YAML-like:\n"+|blockListF xs|+"") YAML-like: - John - Bob >>> fmt ("JSON-like: "+|jsonListF xs|+"") JSON-like: [ John , Bob ] -} {- $migration Instead of using @%@, surround variables with '+|' and '|+'. You don't have to use @sformat@ or anything else, and also where you were using @build@, @int@, @text@, etc in @formatting@, you don't have to use anything in @fmt@: @ formatting __sformat ("Foo: "%build%", bar: "%int) foo bar__ fmt __"Foo: "+|foo|+", bar: "+|bar|+""__ @ The resulting formatted string is polymorphic and can be used as 'String', 'Text', 'Builder' or even 'IO' (i.e. the string will be printed to the screen). However, when printing it is recommended to use 'fmt' or 'fmtLn' for clarity. @fmt@ provides lots of formatters (which are simply functions that produce 'Builder'): @ formatting __sformat ("Got another byte ("%hex%")") x__ fmt __"Got another byte ("+|hexF x|+")"__ @ Instead of the @shown@ formatter, either just use 'show' or double brackets: @ formatting __sformat ("This uses Show: "%shown%") foo__ fmt #1 __"This uses Show: "+|show foo|+""__ fmt #2 __"This uses Show: "+||foo||+""__ @ Many formatters from @formatting@ have the same names in @fmt@, but with added “F”: 'hexF', 'exptF', etc. Some have been renamed, though: @ __Cutting:__ fitLeft -\> 'prefixF' fitRight -\> 'suffixF' __Padding:__ left -\> 'padLeftF' right -\> 'padRightF' center -\> 'padBothF' __Stuff with numbers:__ ords -\> 'ordinalF' commas -\> 'commaizeF' @ Also, some formatters from @formatting@ haven't been added to @fmt@ yet. Specifically: * @plural@ and @asInt@ (but instead of @asInt@ you can use 'fromEnum') * @prefixBin@, @prefixOrd@, @prefixHex@, and @bytes@ * formatters that use @Scientific@ (@sci@ and @scifmt@) * formatters that deal with time (anything from @Formatting.Time@) They will be added later. (On the other hand, @fmt@ provides some useful formatters not available in @formatting@, such as 'listF', 'mapF', 'tupleF' and so on.) -} ---------------------------------------------------------------------------- -- Operators with 'Buildable' ---------------------------------------------------------------------------- {- $brackets To format strings, put variables between ('+|') and ('|+'): >>> let name = "Alice" >>> "Meet "+|name|+"!" :: String "Meet Alice!" Of course, 'Text' is supported as well: >>> "Meet "+|name|+"!" :: Text "Meet Alice!" You don't actually need any type signatures; however, if you're toying with this library in GHCi, it's recommended to either add a type signature or use 'fmtLn': >>> fmtLn ("Meet "+|name|+"!") Meet Alice! Otherwise the type of the formatted string would be resolved to @IO ()@ and printed without a newline, which is not very convenient when you're in GHCi. On the other hand, it's useful for quick-and-dirty scripts: @ main = do [fin, fout] \<- words \<$\> getArgs __"Reading data from "+|fin|+"\\n"__ xs \<- readFile fin __"Writing processed data to "+|fout|+"\\n"__ writeFile fout (show (process xs)) @ Anyway, let's proceed. Anything 'Buildable', including numbers, booleans, characters and dates, can be put between ('+|') and ('|+'): >>> let starCount = "173" >>> fmtLn ("Meet "+|name|+"! She's got "+|starCount|+" stars on Github.") "Meet Alice! She's got 173 stars on Github." Since the only thing ('+|') and ('|+') do is concatenate strings and do conversion, you can use any functions you want inside them. In this case, 'length': >>> fmtLn (""+|name|+"'s name has "+|length name|+" letters") Alice's name has 5 letters If something isn't 'Buildable', just use 'show' on it: >>> let pos = (3, 5) >>> fmtLn ("Character's position: "+|show pos|+"") Character's position: (3,5) Or one of many formatters provided by this library – for instance, for tuples of various sizes there's 'tupleF': >>> fmtLn ("Character's position: "+|tupleF pos|+"") Character's position: (3, 5) Finally, for convenience there's the ('|++|') operator, which can be used if you've got one variable following the other: >>> let (a, op, b, res) = (2, "*", 2, 4) >>> fmtLn (""+|a|++|op|++|b|+" = "+|res|+"") 2*2 = 4 Also, since in some codebases there are /lots/ of types which aren't 'Buildable', there are operators ('+||') and ('||+'), which use 'show' instead of 'build': @ __(""+|show foo|++|show bar|+"")__ === __(""+||foo||++||bar||+"")__ @ -} -- $god1 -- Operators for the operators god! -- | Concatenate, then convert (+|) :: (FromBuilder b) => Builder -> Builder -> b (+|) str rest = fromBuilder (str <> rest) -- | 'build' and concatenate, then convert (|+) :: (Buildable a, FromBuilder b) => a -> Builder -> b (|+) a rest = fromBuilder (build a <> rest) infixr 1 +| infixr 1 |+ ---------------------------------------------------------------------------- -- Operators with 'Show' ---------------------------------------------------------------------------- -- $god2 -- More operators for the operators god! -- | Concatenate, then convert (+||) :: (FromBuilder b) => Builder -> Builder -> b (+||) str rest = str +| rest {-# INLINE (+||) #-} -- | 'show' and concatenate, then convert (||+) :: (Show a, FromBuilder b) => a -> Builder -> b (||+) a rest = show a |+ rest {-# INLINE (||+) #-} infixr 1 +|| infixr 1 ||+ ---------------------------------------------------------------------------- -- Combinations ---------------------------------------------------------------------------- {- $god3 Z̸͠A̵̕͟͠L̡̀́͠G̶̛O͝ ̴͏̀ I͞S̸̸̢͠  ̢̛͘͢C̷͟͡Ó̧̨̧͞M̡͘͟͞I̷͜N̷̕G̷̀̕ (Though you can just use @""@ between @+| |+@ instead of using these operators, and Show-brackets don't have to be used at all because there's 'show' available.) -} (|++|) :: (Buildable a, FromBuilder b) => a -> Builder -> b (|++|) a rest = fromBuilder (build a <> rest) {-# INLINE (|++|) #-} (||++||) :: (Show a, FromBuilder b) => a -> Builder -> b (||++||) a rest = show a |+ rest {-# INLINE (||++||) #-} (||++|) :: (Buildable a, FromBuilder b) => a -> Builder -> b (||++|) a rest = a |++| rest {-# INLINE (||++|) #-} (|++||) :: (Show a, FromBuilder b) => a -> Builder -> b (|++||) a rest = a ||++|| rest {-# INLINE (|++||) #-} infixr 1 |++| infixr 1 ||++|| infixr 1 ||++| infixr 1 |++|| ---------------------------------------------------------------------------- -- Old-style formatting ---------------------------------------------------------------------------- {- | An old-style formatting function taken from @text-format@ (see "Data.Text.Format"). Unlike 'Data.Text.Format.format' from "Data.Text.Format", it can produce 'String' and strict 'Text' as well (and print to console too). Also it's polyvariadic: >>> format "{} + {} = {}" 2 2 4 "2 + 2 = 4" You can use arbitrary formatters: >>> format "0x{} + 0x{} = 0x{}" (hexF 130) (hexF 270) (hexF (130+270)) "0x82 + 0x10e = 0x190" -} format :: FormatType r => TF.Format -> r format f = format' f [] {-# INLINE format #-} {- | Like 'format', but adds a newline. -} formatLn :: FormatType r => TF.Format -> r formatLn f = format' (f <> "\n") [] {-# INLINE formatLn #-} ---------------------------------------------------------------------------- -- Helper functions ---------------------------------------------------------------------------- {- | 'fmt' converts things to 'String', 'Text' or 'Builder'. Most of the time you won't need it, as strings produced with ('+|') and ('|+') can already be used as 'String', 'Text', etc. However, combinators like 'listF' can only produce 'Builder' (for better type inference), and you need to use 'fmt' on them. Also, 'fmt' can do printing: >>> fmt "Hello world!\n" Hello world! -} fmt :: FromBuilder b => Builder -> b fmt = fromBuilder {-# INLINE fmt #-} {- | Like 'fmt', but appends a newline. -} fmtLn :: FromBuilder b => Builder -> b fmtLn = fromBuilder . (<> "\n") {-# INLINE fmtLn #-} ---------------------------------------------------------------------------- -- Formatters ---------------------------------------------------------------------------- {- | Attach a name to anything: >>> fmt $ nameF "clients" $ blockListF ["Alice", "Bob", "Zalgo"] clients: - Alice - Bob - Zalgo -} nameF :: Builder -> Builder -> Builder nameF k v = case TL.lines (toLazyText v) of [] -> k <> ":\n" [l] -> k <> ": " <> fromLazyText l <> "\n" ls -> k <> ":\n" <> mconcat [" " <> fromLazyText s <> "\n" | s <- ls] ---------------------------------------------------------------------------- -- List formatters ---------------------------------------------------------------------------- {- | A simple comma-separated list formatter. >>> listF ["hello", "world"] "[hello, world]" For multiline output, use 'jsonListF'. -} listF :: (Foldable f, Buildable a) => f a -> Builder listF = listF' build {-# INLINE listF #-} {- | A version of 'listF' that lets you supply your own building function for list elements. For instance, to format a list of lists you'd have to do this (since there's no 'Buildable' instance for lists): >>> listF' listF [[1,2,3],[4,5,6]] "[[1, 2, 3], [4, 5, 6]]" -} listF' :: (Foldable f) => (a -> Builder) -> f a -> Builder listF' fbuild xs = mconcat $ "[" : intersperse ", " (map fbuild (toList xs)) ++ ["]"] {-# SPECIALIZE listF' :: (a -> Builder) -> [a] -> Builder #-} {- Note [Builder appending] ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The documentation for 'Builder' says that it's preferrable to associate 'Builder' appends to the right (i.e. @a <> (b <> c)@). The maximum possible association-to-the-right is achieved when we avoid appending builders until the last second (i.e. in the latter scenario): -- (a1 <> x) <> (a2 <> x) <> ... mconcat [a <> x | a <- as] -- a1 <> x <> a2 <> x <> ... mconcat $ concat [[a, x] | a <- as] However, benchmarks have shown that the former way is actually faster. -} {- | A multiline formatter for lists. >>> fmt $ blockListF [1,2,3] - 1 - 2 - 3 It automatically handles multiline list elements: @ >>> __fmt $ blockListF ["hello\\nworld", "foo\\nbar\\nquix"]__ - hello world - foo bar quix @ -} blockListF :: forall f a. (Foldable f, Buildable a) => f a -> Builder blockListF = blockListF' build {-# INLINE blockListF #-} {- | A version of 'blockListF' that lets you supply your own building function for list elements. -} blockListF' :: forall f a. (Foldable f) => (a -> Builder) -> f a -> Builder blockListF' fbuild xs | null items = "[]\n" | True `elem` mls = mconcat (intersperse "\n" items) | otherwise = mconcat items where (mls, items) = unzip $ map buildItem (toList xs) -- Returns 'True' if the item is multiline buildItem :: a -> (Bool, Builder) buildItem x = case TL.lines (toLazyText (fbuild x)) of [] -> (False, "-\n") (l:ls) -> (not (null ls), "- " <> fromLazyText l <> "\n" <> mconcat [" " <> fromLazyText s <> "\n" | s <- ls]) {-# SPECIALIZE blockListF' :: (a -> Builder) -> [a] -> Builder #-} {- | A JSON-style formatter for lists. >>> fmt $ jsonListF [1,2,3] [ 1 , 2 , 3 ] Like 'blockListF', it handles multiline elements well: >>> fmt $ jsonListF ["hello\nworld", "foo\nbar\nquix"] [ hello world , foo bar quix ] (Note that, unlike 'blockListF', it doesn't add blank lines in such cases.) -} jsonListF :: forall f a. (Foldable f, Buildable a) => f a -> Builder jsonListF = jsonListF' build {-# INLINE jsonListF #-} {- | A version of 'jsonListF' that lets you supply your own building function for list elements. -} jsonListF' :: forall f a. (Foldable f) => (a -> Builder) -> f a -> Builder jsonListF' fbuild xs | null items = "[]\n" | otherwise = "[\n" <> mconcat items <> "]\n" where items = zipWith buildItem (True : repeat False) (toList xs) -- Item builder buildItem :: Bool -> a -> Builder buildItem isFirst x = case map fromLazyText (TL.lines (toLazyText (fbuild x))) of [] | isFirst -> "\n" | otherwise -> ",\n" ls -> mconcat . map (<> "\n") $ ls & _head %~ (if isFirst then (" " <>) else (", " <>)) & _tail.each %~ (" " <>) {-# SPECIALIZE jsonListF' :: (a -> Builder) -> [a] -> Builder #-} ---------------------------------------------------------------------------- -- Map formatters ---------------------------------------------------------------------------- #if __GLASGOW_HASKELL__ >= 708 # define MAPTOLIST IsList.toList #else # define MAPTOLIST id #endif {- | A simple JSON-like map formatter; works for Map, HashMap, etc, as well as ordinary lists of pairs. >>> mapF [("a", 1), ("b", 4)] "{a: 1, b: 4}" For multiline output, use 'jsonMapF'. -} mapF :: #if __GLASGOW_HASKELL__ >= 708 (IsList t, Item t ~ (k, v), Buildable k, Buildable v) => t -> Builder #else (Buildable k, Buildable v) => [(k, v)] -> Builder #endif mapF = mapF' build build {-# INLINE mapF #-} {- | A version of 'mapF' that lets you supply your own building function for keys and values. -} mapF' :: #if __GLASGOW_HASKELL__ >= 708 (IsList t, Item t ~ (k, v)) => (k -> Builder) -> (v -> Builder) -> t -> Builder #else (k -> Builder) -> (v -> Builder) -> [(k, v)] -> Builder #endif mapF' fbuild_k fbuild_v xs = "{" <> mconcat (intersperse ", " (map buildPair (MAPTOLIST xs))) <> "}" where buildPair (k, v) = fbuild_k k <> ": " <> fbuild_v v {- | A YAML-like map formatter: >>> fmt $ blockMapF [("Odds", blockListF [1,3]), ("Evens", blockListF [2,4])] Odds: - 1 - 3 Evens: - 2 - 4 -} blockMapF :: #if __GLASGOW_HASKELL__ >= 708 (IsList t, Item t ~ (k, v), Buildable k, Buildable v) => t -> Builder #else (Buildable k, Buildable v) => [(k, v)] -> Builder #endif blockMapF = blockMapF' build build {-# INLINE blockMapF #-} {- | A version of 'blockMapF' that lets you supply your own building function for keys and values. -} blockMapF' :: #if __GLASGOW_HASKELL__ >= 708 (IsList t, Item t ~ (k, v)) => (k -> Builder) -> (v -> Builder) -> t -> Builder #else (k -> Builder) -> (v -> Builder) -> [(k, v)] -> Builder #endif blockMapF' fbuild_k fbuild_v xs | null items = "{}\n" | otherwise = mconcat items where items = map (\(k, v) -> nameF (fbuild_k k) (fbuild_v v)) (MAPTOLIST xs) {- | A JSON-like map formatter (unlike 'mapF', always multiline): >>> fmt $ jsonMapF [("Odds", jsonListF [1,3]), ("Evens", jsonListF [2,4])] { Odds: [ 1 , 3 ] , Evens: [ 2 , 4 ] } -} jsonMapF :: #if __GLASGOW_HASKELL__ >= 708 (IsList t, Item t ~ (k, v), Buildable k, Buildable v) => t -> Builder #else (Buildable k, Buildable v) => [(k, v)] -> Builder #endif jsonMapF = jsonMapF' build build {-# INLINE jsonMapF #-} {- | A version of 'jsonMapF' that lets you supply your own building function for keys and values. -} jsonMapF' :: #if __GLASGOW_HASKELL__ >= 708 forall t k v. (IsList t, Item t ~ (k, v)) => (k -> Builder) -> (v -> Builder) -> t -> Builder #else forall k v. (k -> Builder) -> (v -> Builder) -> [(k, v)] -> Builder #endif jsonMapF' fbuild_k fbuild_v xs | null items = "{}\n" | otherwise = "{\n" <> mconcat items <> "}\n" where items = zipWith buildItem (True : repeat False) (MAPTOLIST xs) -- Item builder buildItem :: Bool -> (k, v) -> Builder buildItem isFirst (k, v) = do let kb = (if isFirst then " " else ", ") <> fbuild_k k case map fromLazyText (TL.lines (toLazyText (fbuild_v v))) of [] -> kb <> ":\n" [l] -> kb <> ": " <> l <> "\n" ls -> kb <> ":\n" <> mconcat [" " <> s <> "\n" | s <- ls] ---------------------------------------------------------------------------- -- Tuple formatters ---------------------------------------------------------------------------- instance (Buildable a1, Buildable a2) => TupleF (a1, a2) where tupleF (a1, a2) = tupleLikeF [build a1, build a2] instance (Buildable a1, Buildable a2, Buildable a3) => TupleF (a1, a2, a3) where tupleF (a1, a2, a3) = tupleLikeF [build a1, build a2, build a3] instance (Buildable a1, Buildable a2, Buildable a3, Buildable a4) => TupleF (a1, a2, a3, a4) where tupleF (a1, a2, a3, a4) = tupleLikeF [build a1, build a2, build a3, build a4] instance (Buildable a1, Buildable a2, Buildable a3, Buildable a4, Buildable a5) => TupleF (a1, a2, a3, a4, a5) where tupleF (a1, a2, a3, a4, a5) = tupleLikeF [build a1, build a2, build a3, build a4, build a5] instance (Buildable a1, Buildable a2, Buildable a3, Buildable a4, Buildable a5, Buildable a6) => TupleF (a1, a2, a3, a4, a5, a6) where tupleF (a1, a2, a3, a4, a5, a6) = tupleLikeF [build a1, build a2, build a3, build a4, build a5, build a6] instance (Buildable a1, Buildable a2, Buildable a3, Buildable a4, Buildable a5, Buildable a6, Buildable a7) => TupleF (a1, a2, a3, a4, a5, a6, a7) where tupleF (a1, a2, a3, a4, a5, a6, a7) = tupleLikeF [build a1, build a2, build a3, build a4, build a5, build a6, build a7] instance (Buildable a1, Buildable a2, Buildable a3, Buildable a4, Buildable a5, Buildable a6, Buildable a7, Buildable a8) => TupleF (a1, a2, a3, a4, a5, a6, a7, a8) where tupleF (a1, a2, a3, a4, a5, a6, a7, a8) = tupleLikeF [build a1, build a2, build a3, build a4, build a5, build a6, build a7, build a8] {- | Format a list like a tuple. (This function is used to define 'tupleF'.) -} tupleLikeF :: [Builder] -> Builder tupleLikeF xs | True `elem` mls = mconcat (intersperse ",\n" items) | otherwise = "(" <> mconcat (intersperse ", " xs) <> ")" where (mls, items) = unzip $ zipWith3 buildItem xs (set _head True falses) (set _last True falses) -- A list of 'False's which has the same length as 'xs' falses = map (const False) xs -- Returns 'True' if the item is multiline buildItem :: Builder -> Bool -- ^ Is the item the first? -> Bool -- ^ Is the item the last? -> (Bool, Builder) buildItem x isFirst isLast = case map fromLazyText (TL.lines (toLazyText x)) of [] | isFirst && isLast -> (False, "()\n") | isFirst -> (False, "(\n") | isLast -> (False, " )\n") | otherwise -> (False, "") ls -> (not (null (tail ls)), mconcat . map (<> "\n") $ ls & _head %~ (if isFirst then ("( " <>) else (" " <>)) & _tail.each %~ (" " <>) & _last %~ (if isLast then (<> " )") else id)) ---------------------------------------------------------------------------- -- ADT formatters ---------------------------------------------------------------------------- {- | Like 'build' for 'Maybe', but displays 'Nothing' as @\@ instead of an empty string. 'build': >>> build (Nothing :: Maybe Int) "" >>> build (Just 1 :: Maybe Int) "1" 'maybeF': >>> maybeF (Nothing :: Maybe Int) "" >>> maybeF (Just 1 :: Maybe Int) "1" -} maybeF :: Buildable a => Maybe a -> Builder maybeF = maybe "" build {- | Format an 'Either': >>> eitherF (Right 1) "" -} eitherF :: (Buildable a, Buildable b) => Either a b -> Builder eitherF = either (\x -> " build x <> ">") (\x -> " build x <> ">") ---------------------------------------------------------------------------- -- Other formatters ---------------------------------------------------------------------------- {- | Take the first N characters: >>> prefixF 3 "hello" "hel" -} prefixF :: Buildable a => Int -> a -> Builder prefixF size = fromLazyText . TL.take (fromIntegral size) . toLazyText . build {- | Take the last N characters: >>> suffixF 3 "hello" "llo" -} suffixF :: Buildable a => Int -> a -> Builder suffixF size = fromLazyText . (\t -> TL.drop (TL.length t - fromIntegral size) t) . toLazyText . build {- | @padLeftF n c@ pads the string with character @c@ from the left side until it becomes @n@ characters wide (and does nothing if the string is already that long, or longer): >>> padLeftF 5 '0' 12 "00012" >>> padLeftF 5 '0' 123456 "123456" -} padLeftF :: Buildable a => Int -> Char -> a -> Builder padLeftF = TF.left {- | @padRightF n c@ pads the string with character @c@ from the right side until it becomes @n@ characters wide (and does nothing if the string is already that long, or longer): >>> padRightF 5 ' ' "foo" "foo " >>> padRightF 5 ' ' "foobar" "foobar" -} padRightF :: Buildable a => Int -> Char -> a -> Builder padRightF = TF.right {- | @padBothF n c@ pads the string with character @c@ from both sides until it becomes @n@ characters wide (and does nothing if the string is already that long, or longer): >>> padBothF 5 '=' "foo" "=foo=" >>> padBothF 5 '=' "foobar" "foobar" When padding can't be distributed equally, the left side is preferred: >>> padBoth 8 '=' "foo" "===foo==" -} padBothF :: Buildable a => Int -> Char -> a -> Builder padBothF i c = fromLazyText . TL.center (fromIntegral i) c . toLazyText . build {- | Break digits in a number: >>> commaizeF 15830000 "15,830,000" -} commaizeF :: (Buildable a, Integral a) => a -> Builder commaizeF = groupInt 3 ',' {- | Format a number as octal: >>> listF' octF [7,8,9,10] "[7, 10, 11, 12]" -} octF :: Integral a => a -> Builder octF = baseF 8 {- | Format a number as binary: >>> listF' binF [7,8,9,10] "[111, 1000, 1001, 1010]" -} binF :: Integral a => a -> Builder binF = baseF 2 {- | Format a number in arbitrary base (up to 36): >>> baseF 3 10000 "111201101" >>> baseF 7 10000 "41104" >>> baseF 36 10000 "7ps" -} baseF :: Integral a => Int -> a -> Builder baseF numBase = build . atBase numBase {- | Format a floating-point number: >>> floatF 3.1415 "3.1415" Numbers bigger than 1e21 or smaller than 1e-6 will be displayed using scientific notation: >>> listF' floatF [1e-6,9e-7] "[0.000001, 9e-7]" >>> listF' floatF [9e20,1e21] "[900000000000000000000, 1e21]" -} floatF :: Real a => a -> Builder floatF = TF.shortest {- | Format a floating-point number using scientific notation, with given amount of precision: >>> listF' (exptF 5) [pi,0.1,10] "[3.14159e0, 1.00000e-1, 1.00000e1]" -} exptF :: Real a => Int -> a -> Builder exptF = TF.expt {- | Format a floating-point number with given amount of precision. For small numbers, it uses scientific notation for everything smaller than 1e-6: >>> listF' (precF 3) [1e-5,1e-6,1e-7] "[0.0000100, 0.00000100, 1.00e-7]" For large numbers, it uses scientific notation for everything larger than 1eN, where N is the precision: >>> listF' (precF 4) [1e3,5e3,1e4] "[1000, 5000, 1.000e4]" -} precF :: Real a => Int -> a -> Builder precF = TF.prec ---------------------------------------------------------------------------- -- Conditional formatters ---------------------------------------------------------------------------- {- | Display something only if the condition is 'True' (empty string otherwise). @ >>> __"Hello!" <> whenF showDetails (", details: "+|foobar|+"")__ @ Note that it can only take a 'Builder' (because otherwise it would be unusable with ('+|')-formatted strings which can resolve to any 'FromBuilder'). Thus, use 'fmt' if you need just one value: @ >>> __"Maybe here's a number: "+|whenF cond (fmt n)|+""__ @ -} whenF :: Bool -> Builder -> Builder whenF True x = x whenF False _ = mempty {-# INLINE whenF #-} {- | Display something only if the condition is 'False' (empty string otherwise). -} unlessF :: Bool -> Builder -> Builder unlessF False x = x unlessF True _ = mempty {-# INLINE unlessF #-} ---------------------------------------------------------------------------- -- Utilities ---------------------------------------------------------------------------- {- | Indent already formatted text. >>> fmt $ "This is a list:\n" <> indent 4 (blockListF [1,2,3]) This is a list: - 1 - 2 - 3 The output will always end with a newline, even when the input doesn't. -} indent :: Int -> Builder -> Builder indent n a = case TL.lines (toLazyText a) of [] -> fromLazyText (spaces <> "\n") xs -> fromLazyText $ TL.unlines (map (spaces <>) xs) where spaces = TL.replicate (fromIntegral n) (TL.singleton ' ') ---------------------------------------------------------------------------- -- Generic formatting ---------------------------------------------------------------------------- {- | Format an arbitrary value without requiring a 'Buildable' instance: @ data Foo = Foo { x :: Bool, y :: [Int] } deriving Generic @ >>> fmt (genericF (Foo True [1,2,3])) Foo: x: True y: [1, 2, 3] It works for non-record constructors too: @ data Bar = Bar Bool [Int] deriving Generic @ >>> fmtLn (genericF (Bar True [1,2,3])) Any fields inside the type must either be 'Buildable' or one of the following types: * a function * a tuple (up to 8-tuples) * list, 'NonEmpty', 'Seq' * 'Map', 'IntMap', 'Set', 'IntSet' * 'Maybe', 'Either' The exact format of 'genericF' might change in future versions, so don't rely on it. It's merely a convenience function. -} genericF :: (Generic a, GBuildable (Rep a)) => a -> Builder genericF = gbuild . from instance GBuildable a => GBuildable (M1 D d a) where gbuild (M1 x) = gbuild x instance (GetFields a, Constructor c) => GBuildable (M1 C c a) where -- A note on fixity: -- * Ordinarily e.g. "Foo" is prefix and e.g. ":|" is infix -- * However, "Foo" can be infix when defined as "a `Foo` b" -- * And ":|" can be prefix when defined as "(:|) a b" gbuild c@(M1 x) = case conFixity c of Infix _ _ | [a, b] <- fields -> format "({} {} {})" a infixName b -- this case should never happen, but still | otherwise -> format "<{}: {}>" prefixName (mconcat (intersperse ", " fields)) Prefix | isTuple -> tupleLikeF fields | conIsRecord c -> nameF (build prefixName) (blockMapF fieldsWithNames) | null (getFields x) -> build prefixName -- I believe that there will be only one field in this case | null (conName c) -> mconcat (intersperse ", " fields) | otherwise -> format "<{}: {}>" prefixName (mconcat (intersperse ", " fields)) where (prefixName, infixName) | ":" `isPrefixOf` conName c = ("(" ++ conName c ++ ")", conName c) | otherwise = (conName c, "`" ++ conName c ++ "`") fields = map snd (getFields x) fieldsWithNames = getFields x isTuple = "(," `isPrefixOf` prefixName instance Buildable' c => GBuildable (K1 i c) where gbuild (K1 a) = build' a instance (GBuildable a, GBuildable b) => GBuildable (a :+: b) where gbuild (L1 x) = gbuild x gbuild (R1 x) = gbuild x instance (GetFields a, GetFields b) => GetFields (a :*: b) where getFields (a :*: b) = getFields a ++ getFields b instance (GBuildable a, Selector c) => GetFields (M1 S c a) where getFields s@(M1 a) = [(selName s, gbuild a)] instance GBuildable a => GetFields (M1 D c a) where getFields (M1 a) = [("", gbuild a)] instance GBuildable a => GetFields (M1 C c a) where getFields (M1 a) = [("", gbuild a)] instance GetFields U1 where getFields _ = [] ---------------------------------------------------------------------------- -- A more powerful Buildable used for genericF ---------------------------------------------------------------------------- instance Buildable' () where build' _ = "()" instance (Buildable' a1, Buildable' a2) => Buildable' (a1, a2) where build' (a1, a2) = tupleLikeF [build' a1, build' a2] instance (Buildable' a1, Buildable' a2, Buildable' a3) => Buildable' (a1, a2, a3) where build' (a1, a2, a3) = tupleLikeF [build' a1, build' a2, build' a3] instance (Buildable' a1, Buildable' a2, Buildable' a3, Buildable' a4) => Buildable' (a1, a2, a3, a4) where build' (a1, a2, a3, a4) = tupleLikeF [build' a1, build' a2, build' a3, build' a4] instance (Buildable' a1, Buildable' a2, Buildable' a3, Buildable' a4, Buildable' a5) => Buildable' (a1, a2, a3, a4, a5) where build' (a1, a2, a3, a4, a5) = tupleLikeF [build' a1, build' a2, build' a3, build' a4, build' a5] instance (Buildable' a1, Buildable' a2, Buildable' a3, Buildable' a4, Buildable' a5, Buildable' a6) => Buildable' (a1, a2, a3, a4, a5, a6) where build' (a1, a2, a3, a4, a5, a6) = tupleLikeF [build' a1, build' a2, build' a3, build' a4, build' a5, build' a6] instance (Buildable' a1, Buildable' a2, Buildable' a3, Buildable' a4, Buildable' a5, Buildable' a6, Buildable' a7) => Buildable' (a1, a2, a3, a4, a5, a6, a7) where build' (a1, a2, a3, a4, a5, a6, a7) = tupleLikeF [build' a1, build' a2, build' a3, build' a4, build' a5, build' a6, build' a7] instance (Buildable' a1, Buildable' a2, Buildable' a3, Buildable' a4, Buildable' a5, Buildable' a6, Buildable' a7, Buildable' a8) => Buildable' (a1, a2, a3, a4, a5, a6, a7, a8) where build' (a1, a2, a3, a4, a5, a6, a7, a8) = tupleLikeF [build' a1, build' a2, build' a3, build' a4, build' a5, build' a6, build' a7, build' a8] instance _OVERLAPPING_ Buildable' [Char] where build' = build instance Buildable' a => Buildable' [a] where build' = listF' build' #if MIN_VERSION_base(4,9,0) instance Buildable' a => Buildable' (NonEmpty a) where build' = listF' build' #endif instance Buildable' a => Buildable' (Seq a) where build' = listF' build' instance (Buildable' k, Buildable' v) => Buildable' (Map k v) where build' = mapF' build' build' . Map.toList instance (Buildable' v) => Buildable' (Set v) where build' = listF' build' instance (Buildable' v) => Buildable' (IntMap v) where build' = mapF' build' build' . IntMap.toList instance Buildable' IntSet where build' = listF' build' . IntSet.toList instance (Buildable' a) => Buildable' (Maybe a) where build' Nothing = maybeF (Nothing :: Maybe Builder) build' (Just a) = maybeF (Just (build' a) :: Maybe Builder) instance (Buildable' a, Buildable' b) => Buildable' (Either a b) where build' (Left a) = eitherF (Left (build' a) :: Either Builder Builder) build' (Right a) = eitherF (Right (build' a) :: Either Builder Builder) instance Buildable' (a -> b) where build' _ = "" instance _OVERLAPPABLE_ Buildable a => Buildable' a where build' = build ---------------------------------------------------------------------------- -- TODOs ---------------------------------------------------------------------------- {- list/map ~~~~~~~~~~~~~~~~~~~~ * maybe add something like blockMapF_ and blockListF_ that would add a blank line automatically? or `---` and `:::` or something? * should also add something to _not_ add a blank line between list entries (e.g. when they are 'name'd and can be clearly differentiated) -} {- docs ~~~~~~~~~~~~~~~~~~~~ * write explicitly that 'build' can be used and is useful sometimes * mention that fmt doesn't do the neat thing that formatting does with (<>) (or maybe it does? there's a monoid instance for functions after all, though I might also have to write a IsString instance for (a -> Builder)) * write that if +| |+ are hated or if it's inconvenient in some cases, you can just use provided formatters and <> (add Fmt.DIY for that?) (e.g. "pub:" <> base16F foo) * write that it can be used in parallel with formatting? can it, actually? * clarify what exactly is hard about writing `formatting` formatters -} {- others ~~~~~~~~~~~~~~~~~~~~ * what effect does it have on compilation time? what effect do other formatting libraries have on compilation time? -}