packages feed

format-heavy (empty) → 0.1.0.2

raw patch · 19 files changed

+2001/−0 lines, 19 filesdep +basedep +bytestringdep +containerssetup-changed

Dependencies added: base, bytestring, containers, data-default, format-heavy, hspec, labels, parsec, template-haskell, text, th-lift, th-lift-instances, time

Files

+ ChangeLog.md view
@@ -0,0 +1,13 @@+# Revision history for format-heavy++## 0.1.0.2  -- 2026-09-11++* Switch development and CI tooling to Cabal only.++## 0.1.0.1  -- 2026-09-01++* Add CI coverage for GHC 9.2 through 9.10.++## 0.1.0.0  -- 2026-08-31++* Initial release of format-heavy.
+ Data/Text/Format/Heavy.hs view
@@ -0,0 +1,39 @@+-- | This is the main module of @format-heavy@ library.+--+-- In most cases, you need to import only this module, and probably also the+-- Data.Text.Format.Heavy.Time module, if you want to format time/date values.+--+-- This package exports the @format@ function and @Format@ data type.+-- The Format type implements the instance of IsString, so in the code you may+-- use formatting strings as literals, if you enable @OverloadedStrings@ extension.+--+-- Formatting strings syntax is based on Python's string.format() syntax.+--+-- The simple usage example is+--+-- @+-- {-\# LANGUAGE OverloadedStrings #\-}+-- module Main where+--+-- import Data.Time+-- import qualified Data.Text.Lazy.IO as TLIO+-- import Data.Text.Format.Heavy+-- import Data.Text.Format.Heavy.Time+--+-- main :: IO ()+-- main = do+--   name <- getLine+--   time <- getZonedTime+--   TLIO.putStrLn $ format "Hello, {}! It is {:%H:%M:%S} now." (name, time)+-- @+module Data.Text.Format.Heavy (+  module Data.Text.Format.Heavy.Types,+  module Data.Text.Format.Heavy.Formats,+  module Data.Text.Format.Heavy.Build,+  module Data.Text.Format.Heavy.Instances,+) where++import Data.Text.Format.Heavy.Build (format)+import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Instances hiding (genericFloatFormat, genericIntFormat)+import Data.Text.Format.Heavy.Types
+ Data/Text/Format/Heavy/Build.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Text.Format.Heavy.Build (+  format,+  formatEither,+  makeBuilder,++  -- * Formatters building utilities+  align,+  applySign,+  applySharp,+  convertText,+  formatInt,+  formatStr,+  formatFloat,+  formatBool,+)+where++import Control.Monad+import Data.Maybe+import Data.Monoid+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Data.Text.Lazy.Builder.Int (+  decimal,+  hexadecimal,+ )+import Data.Text.Lazy.Builder.RealFloat++import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Types++makeBuilder :: (VarContainer c) => Format -> c -> Either String B.Builder+makeBuilder (Format items) vars = mconcat `fmap` mapM go items+ where+  go (FString s) = Right $ B.fromLazyText s+  go (FVariable name fmt) = case lookupVar name vars of+    Nothing -> Left $ "Parameter not found: " ++ TL.unpack name+    Just var -> formatVar fmt var+{-# INLINE makeBuilder #-}++-- | The main formatting function.+-- This function throws @error@ if some error detected during format string parsing or formatting itself.+format :: (VarContainer vars) => Format -> vars -> TL.Text+format fmt vars = either error id $ formatEither fmt vars++-- | The main formatting function.+-- This version returns @Left@ value with error description in case of error in+-- format string or error during formatting.+formatEither :: (VarContainer vars) => Format -> vars -> Either String TL.Text+formatEither fmt vars = B.toLazyText `fmap` makeBuilder fmt vars++align' :: Int -> Align -> Char -> B.Builder -> B.Builder+align' width AlignLeft fill text =+  B.fromLazyText $ TL.justifyLeft (fromIntegral width) fill $ B.toLazyText text+align' width AlignRight fill text =+  B.fromLazyText $ TL.justifyRight (fromIntegral width) fill $ B.toLazyText text+align' width AlignCenter fill text =+  B.fromLazyText $ TL.center (fromIntegral width) fill $ B.toLazyText text++-- | Align text within available width according to format+align :: GenericFormat -> B.Builder -> B.Builder+align fmt text = case (gfAlign fmt, gfWidth fmt) of+  (Just a, Just w) -> align' w a (gfFillChar fmt) text+  _ -> text++-- | Add @+/-@ sign to the number representation, if required+applySign :: (Num a, Ord a) => Sign -> a -> B.Builder -> B.Builder+applySign Always x text =+  if x >= 0 then B.singleton '+' <> text else B.singleton '-' <> text+applySign OnlyNegative x text =+  if x >= 0 then text else B.singleton '-' <> text+applySign SpaceForPositive x text =+  if x >= 0 then B.singleton ' ' <> text else B.singleton '-' <> text++-- | Add @0x@ to the number representation, if required+applySharp :: Bool -> Radix -> B.Builder -> B.Builder+applySharp False _ text = text+applySharp True Decimal text = text+applySharp True Hexadecimal text = B.fromLazyText "0x" <> text++-- | Apply text conversion.+convertText :: Maybe Conversion -> B.Builder -> B.Builder+convertText Nothing builder = builder+convertText (Just conv) builder =+  B.fromLazyText+    $ converter+    $ B.toLazyText+      builder+ where+  converter = case conv of+    UpperCase -> TL.toUpper+    LowerCase -> TL.toLower+    TitleCase -> TL.toTitle++-- | Format integer number according to GenericFormat+formatInt :: (Integral a) => GenericFormat -> a -> B.Builder+formatInt fmt x =+  align fmt+    $ applySign (gfSign fmt) x+    $ applySharp+      (gfLeading0x fmt)+      radix+      inRadix+ where+  radix = fromMaybe Decimal (gfRadix fmt)+  conversion = fromMaybe LowerCase (gfConvert fmt)+  inRadix = case radix of+    Decimal -> decimal (abs x)+    Hexadecimal -> case conversion of+      LowerCase -> hexadecimal (abs x)+      _ -> B.fromLazyText . TL.toUpper . B.toLazyText . hexadecimal . abs $ x++-- | Format floating-point number according to GenericFormat+formatFloat :: (RealFloat a) => GenericFormat -> a -> B.Builder+formatFloat fmt x =+  align fmt+    $ applySign (gfSign fmt) x+    $ formatRealFloat Fixed (gfPrecision fmt)+    $ abs x++-- | Format Text according to GenericFormat.+formatStr :: GenericFormat -> TL.Text -> B.Builder+formatStr fmt text =+  convertText (gfConvert fmt) $ align fmt $ B.fromLazyText text++-- | Format boolean value.+formatBool :: BoolFormat -> Bool -> B.Builder+formatBool fmt True = B.fromLazyText $ bfTrue fmt+formatBool fmt False = B.fromLazyText $ bfFalse fmt
+ Data/Text/Format/Heavy/Formats.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains format descriptions for most used variable types.+module Data.Text.Format.Heavy.Formats where++import Data.Default+import qualified Data.Text.Lazy as TL++import Data.Text.Format.Heavy.Types++-- | Alignment of string within specified width+data Align = AlignLeft | AlignRight | AlignCenter+  deriving (Eq, Show)++-- | Whether to show the sign of number+data Sign = Always | OnlyNegative | SpaceForPositive+  deriving (Eq, Show)++-- | Number base+data Radix = Decimal | Hexadecimal+  deriving (Eq, Show)++-- | Supported text conversions+data Conversion+  = UpperCase+  | LowerCase+  | TitleCase+  deriving (Eq, Show)++-- | Generic format description. This is usable for integers, floats and strings.+data GenericFormat = GenericFormat+  { gfFillChar :: Char+  , gfAlign :: Maybe Align+  , gfSign :: Sign+  , gfLeading0x :: Bool+  , gfWidth :: Maybe Int+  , gfPrecision :: Maybe Int+  , gfRadix :: Maybe Radix+  , gfConvert :: Maybe Conversion+  }+  deriving (Eq, Show)++instance Default GenericFormat where+  def =+    GenericFormat+      { gfFillChar = ' '+      , gfAlign = Nothing+      , gfSign = OnlyNegative+      , gfLeading0x = False+      , gfWidth = Nothing+      , gfPrecision = Nothing+      , gfRadix = Nothing+      , gfConvert = Nothing+      }++data BoolFormat = BoolFormat+  { bfTrue :: TL.Text+  , bfFalse :: TL.Text+  }+  deriving (Eq, Show)++instance Default BoolFormat where+  def = BoolFormat "true" "false"
+ Data/Text/Format/Heavy/Instances.hs view
@@ -0,0 +1,492 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE UndecidableInstances #-}++-- | This module contains Formatable and VarContainer instances for most used types.+module Data.Text.Format.Heavy.Instances (+  -- * Utility data types+  Single (..),+  Several (..),+  Shown (..),++  -- * Combinators+  DefaultValue (..),+  ThenCheck (..),+  WithDefault,+  withDefault,+  optional,++  -- * Generic formatters+  genericIntFormat,+  genericFloatFormat,+) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL+import Data.Char+import Data.Default+import Data.Int+import Data.List (union)+import qualified Data.Map as M+import Data.Maybe+import Data.String+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)+import qualified Data.Text.Lazy.Encoding as TLE+import Data.Word++import Data.Text.Format.Heavy.Build+import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Parse+import Data.Text.Format.Heavy.Types++instance IsString Format where+  fromString str = parseFormat' (fromString str)++----------------------- IsVarFormat instances --------------------------------------++instance IsVarFormat GenericFormat where+  parseVarFormat text = either (Left . show) Right $ parseGenericFormat text++instance IsVarFormat BoolFormat where+  parseVarFormat text = either (Left . show) Right $ parseBoolFormat text++---------------------- Generic formatters -------------------------------------------++-- | Generic formatter for integer types+genericIntFormat :: (Integral a) => VarFormat -> a -> Either String B.Builder+genericIntFormat Nothing x = Right $ formatInt def x+genericIntFormat (Just fmtStr) x =+  case parseGenericFormat fmtStr of+    Left err -> Left $ show err+    Right fmt -> Right $ formatInt fmt x++-- | Generic formatter for floating-point types+genericFloatFormat :: (RealFloat a) => VarFormat -> a -> Either String B.Builder+genericFloatFormat Nothing x = Right $ formatFloat def x+genericFloatFormat (Just fmtStr) x =+  case parseGenericFormat fmtStr of+    Left err -> Left $ show err+    Right fmt -> Right $ formatFloat fmt x++------------------------ Formatable instances -------------------------------------------++-- | Unit type is formatted as empty string+instance Formatable () where+  formatVar _ _ = Right mempty++instance Formatable Int where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Int8 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Int16 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Int32 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Int64 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Word8 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Word16 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Word32 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Word64 where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Integer where+  formatVar fmt x = genericIntFormat fmt x++instance Formatable Float where+  formatVar fmt x = genericFloatFormat fmt x++instance Formatable Double where+  formatVar fmt x = genericFloatFormat fmt x++instance Formatable String where+  formatVar Nothing text = Right $ formatStr def (fromString text)+  formatVar (Just fmtStr) text =+    case parseGenericFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatStr fmt (fromString text)++instance Formatable T.Text where+  formatVar Nothing text = Right $ formatStr def $ TL.fromStrict text+  formatVar (Just fmtStr) text =+    case parseGenericFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatStr fmt $ TL.fromStrict text++instance Formatable TL.Text where+  formatVar Nothing text = Right $ formatStr def text+  formatVar (Just fmtStr) text =+    case parseGenericFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatStr fmt text++instance Formatable BS.ByteString where+  formatVar Nothing text = Right $ formatStr def $ TL.fromStrict $ TE.decodeUtf8 text+  formatVar (Just fmtStr) text =+    case parseGenericFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatStr fmt $ TL.fromStrict $ TE.decodeUtf8 text++instance Formatable BSL.ByteString where+  formatVar Nothing text = Right $ formatStr def $ TLE.decodeUtf8 text+  formatVar (Just fmtStr) text =+    case parseGenericFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatStr fmt $ TLE.decodeUtf8 text++instance Formatable Bool where+  formatVar Nothing x = Right $ formatBool def x+  formatVar (Just fmtStr) x =+    case parseBoolFormat fmtStr of+      Left err -> Left $ show err+      Right fmt -> Right $ formatBool fmt x++-- | Container for single parameter.+-- Example usage:+--+-- @+-- format "Hello, {}!" (Single name)+-- @+data Single a = Single {getSingle :: a}+  deriving (Eq, Show)++instance (Formatable a) => Formatable (Single a) where+  formatVar fmt (Single x) = formatVar fmt x++-- | Container for several parameters of the same type.+-- Example usage:+--+-- @+-- format "{} + {} = {}" $ Several [2, 3, 5]+-- @+data Several a = Several {getSeveral :: [a]}+  deriving (Eq, Show)++-- | Values packed in Shown will be formatted using their Show instance.+--+-- For example,+--+-- @+-- formatText "values: {}." (Shown (True, False)) ==> "values: (True, False)."+-- @+data Shown a = Shown {shown :: a}+  deriving (Eq)++instance (Show a) => Show (Shown a) where+  show (Shown x) = show x++instance (Show a) => Formatable (Shown a) where+  formatVar _ (Shown x) = Right $ B.fromLazyText $ TL.pack $ show x++instance (Formatable a) => Formatable (Maybe a) where+  formatVar Nothing Nothing = Right mempty+  formatVar Nothing (Just x) = formatVar Nothing x+  formatVar (Just fmtStr) m =+    case parseMaybeFormat fmtStr of+      Nothing -> case m of+        Nothing -> Right mempty+        Just x -> formatVar (Just fmtStr) x+      Just (xFmtStr, nothingStr) ->+        case m of+          Nothing -> Right $ B.fromLazyText nothingStr+          Just x -> formatVar (Just xFmtStr) x++instance (Formatable a, Formatable b) => Formatable (Either a b) where+  formatVar fmt (Left x) = formatVar fmt x+  formatVar fmt (Right y) = formatVar fmt y++------------------------------- VarContainer instances -------------------------------------++instance (Formatable a) => VarContainer (Single a) where+  lookupVar "0" (Single x) = Just $ Variable x+  lookupVar _ _ = Nothing++instance (Formatable a) => ClosedVarContainer (Single a) where+  allVarNames _ = ["0"]++instance VarContainer () where+  lookupVar _ _ = Nothing++instance ClosedVarContainer () where+  allVarNames _ = []++-- | Maybe container contains one variable (named 0); Nothing contains an empty string.+instance (Formatable a) => VarContainer (Maybe a) where+  lookupVar "0" (Just x) = Just $ Variable x+  lookupVar "0" Nothing = Just $ Variable ()+  lookupVar _ _ = Nothing++instance (Formatable a) => ClosedVarContainer (Maybe a) where+  allVarNames Nothing = []+  allVarNames (Just _) = ["0"]++instance (Formatable a, Formatable b) => VarContainer (a, b) where+  lookupVar "0" (a, _) = Just $ Variable a+  lookupVar "1" (_, b) = Just $ Variable b+  lookupVar _ _ = Nothing++instance (Formatable a, Formatable b) => ClosedVarContainer (a, b) where+  allVarNames _ = ["0", "1"]++instance (Formatable a, Formatable b, Formatable c) => VarContainer (a, b, c) where+  lookupVar "0" (a, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _) = Just $ Variable b+  lookupVar "2" (_, _, c) = Just $ Variable c+  lookupVar _ _ = Nothing++instance (Formatable a, Formatable b, Formatable c) => ClosedVarContainer (a, b, c) where+  allVarNames _ = ["0", "1", "2"]++instance (Formatable a, Formatable b, Formatable c, Formatable d) => VarContainer (a, b, c, d) where+  lookupVar "0" (a, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d) = Just $ Variable d+  lookupVar _ _ = Nothing++instance (Formatable a, Formatable b, Formatable c, Formatable d) => ClosedVarContainer (a, b, c, d) where+  allVarNames _ = ["0", "1", "2", "3"]++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e)+  => VarContainer (a, b, c, d, e)+  where+  lookupVar "0" (a, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e) = Just $ Variable e+  lookupVar _ _ = Nothing++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e)+  => ClosedVarContainer (a, b, c, d, e)+  where+  allVarNames _ = ["0", "1", "2", "3", "4"]++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e, Formatable f)+  => VarContainer (a, b, c, d, e, f)+  where+  lookupVar "0" (a, _, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e, _) = Just $ Variable e+  lookupVar "5" (_, _, _, _, _, f) = Just $ Variable f+  lookupVar _ _ = Nothing++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e, Formatable f)+  => ClosedVarContainer (a, b, c, d, e, f)+  where+  allVarNames _ = ["0", "1", "2", "3", "4", "5"]++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e, Formatable f, Formatable g)+  => VarContainer (a, b, c, d, e, f, g)+  where+  lookupVar "0" (a, _, _, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _, _, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e, _, _) = Just $ Variable e+  lookupVar "5" (_, _, _, _, _, f, _) = Just $ Variable f+  lookupVar "6" (_, _, _, _, _, _, g) = Just $ Variable g+  lookupVar _ _ = Nothing++instance+  (Formatable a, Formatable b, Formatable c, Formatable d, Formatable e, Formatable f, Formatable g)+  => ClosedVarContainer (a, b, c, d, e, f, g)+  where+  allVarNames _ = ["0", "1", "2", "3", "4", "5", "6"]++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  )+  => VarContainer (a, b, c, d, e, f, g, h)+  where+  lookupVar "0" (a, _, _, _, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _, _, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _, _, _, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e, _, _, _) = Just $ Variable e+  lookupVar "5" (_, _, _, _, _, f, _, _) = Just $ Variable f+  lookupVar "6" (_, _, _, _, _, _, g, _) = Just $ Variable g+  lookupVar "7" (_, _, _, _, _, _, _, h) = Just $ Variable h+  lookupVar _ _ = Nothing++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  )+  => ClosedVarContainer (a, b, c, d, e, f, g, h)+  where+  allVarNames _ = ["0", "1", "2", "3", "4", "5", "6", "7"]++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  , Formatable i+  )+  => VarContainer (a, b, c, d, e, f, g, h, i)+  where+  lookupVar "0" (a, _, _, _, _, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _, _, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _, _, _, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _, _, _, _, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e, _, _, _, _) = Just $ Variable e+  lookupVar "5" (_, _, _, _, _, f, _, _, _) = Just $ Variable f+  lookupVar "6" (_, _, _, _, _, _, g, _, _) = Just $ Variable g+  lookupVar "7" (_, _, _, _, _, _, _, h, _) = Just $ Variable h+  lookupVar "8" (_, _, _, _, _, _, _, _, i) = Just $ Variable i+  lookupVar _ _ = Nothing++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  , Formatable i+  )+  => ClosedVarContainer (a, b, c, d, e, f, g, h, i)+  where+  allVarNames _ = ["0", "1", "2", "3", "4", "5", "6", "7", "8"]++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  , Formatable i+  , Formatable j+  )+  => VarContainer (a, b, c, d, e, f, g, h, i, j)+  where+  lookupVar "0" (a, _, _, _, _, _, _, _, _, _) = Just $ Variable a+  lookupVar "1" (_, b, _, _, _, _, _, _, _, _) = Just $ Variable b+  lookupVar "2" (_, _, c, _, _, _, _, _, _, _) = Just $ Variable c+  lookupVar "3" (_, _, _, d, _, _, _, _, _, _) = Just $ Variable d+  lookupVar "4" (_, _, _, _, e, _, _, _, _, _) = Just $ Variable e+  lookupVar "5" (_, _, _, _, _, f, _, _, _, _) = Just $ Variable f+  lookupVar "6" (_, _, _, _, _, _, g, _, _, _) = Just $ Variable g+  lookupVar "7" (_, _, _, _, _, _, _, h, _, _) = Just $ Variable h+  lookupVar "8" (_, _, _, _, _, _, _, _, i, _) = Just $ Variable i+  lookupVar "9" (_, _, _, _, _, _, _, _, _, j) = Just $ Variable j+  lookupVar _ _ = Nothing++instance+  ( Formatable a+  , Formatable b+  , Formatable c+  , Formatable d+  , Formatable e+  , Formatable f+  , Formatable g+  , Formatable h+  , Formatable i+  , Formatable j+  )+  => ClosedVarContainer (a, b, c, d, e, f, g, h, i, j)+  where+  allVarNames _ = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]++instance (Formatable a) => VarContainer (Several a) where+  lookupVar name (Several lst) =+    if not $ TL.all isDigit name+      then Nothing+      else+        let n = read (TL.unpack name)+         in if n >= length lst+              then Nothing+              else Just $ Variable (lst !! n)++instance (Formatable a) => ClosedVarContainer (Several a) where+  allVarNames (Several lst) = map (TL.pack . show) [0 .. length lst - 1]++instance (Formatable x) => VarContainer [(TL.Text, x)] where+  lookupVar name pairs = Variable `fmap` lookup name pairs++instance (Formatable x) => ClosedVarContainer [(TL.Text, x)] where+  allVarNames pairs = map fst pairs++instance (Formatable x) => VarContainer (M.Map TL.Text x) where+  lookupVar name pairs = Variable `fmap` M.lookup name pairs++instance (Formatable x) => ClosedVarContainer (M.Map TL.Text x) where+  allVarNames pairs = M.keys pairs++-- | Variable container which contains fixed value for any variable name.+data DefaultValue = DefaultValue Variable++instance VarContainer DefaultValue where+  lookupVar _ (DefaultValue var) = Just var++-- | Combiled variable container, which uses parameters from @c1@,+-- and if variable is not found there it will check in @c2@.+data ThenCheck c1 c2 = ThenCheck c1 c2++-- | Convenience type synonym.+type WithDefault c = ThenCheck c DefaultValue++instance (VarContainer c1, VarContainer c2) => VarContainer (ThenCheck c1 c2) where+  lookupVar name (ThenCheck c1 c2) =+    case lookupVar name c1 of+      Just result -> Just result+      Nothing -> lookupVar name c2++instance (ClosedVarContainer c1, ClosedVarContainer c2) => ClosedVarContainer (ThenCheck c1 c2) where+  allVarNames (ThenCheck c1 c2) =+    allVarNames c1 `union` allVarNames c2++-- | Use variables from specified container, or use default value if+-- variable is not found in container.+withDefault :: (VarContainer c) => c -> Variable -> WithDefault c+withDefault c value = c `ThenCheck` DefaultValue value++-- | Use variables from specified container, or use empty string+-- variable is not found in container.+optional :: (VarContainer c) => c -> WithDefault c+optional c = c `withDefault` (Variable TL.empty)
+ Data/Text/Format/Heavy/Parse.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains parsers for formatting strings.+-- We have to deal with two kinds of strings:+--+-- * String formats. This is the whole construct like @"Hello, {}! Your account balance is {1:+8.4}."@.+-- * Variable formats. This is only part after colon in braces, i.e. the @+8.4@ thing in previous example.+--+-- The string format syntax is supposed to be very stable and simple.+-- There are more than one commonly used formatting string syntax, though. This package provides the+-- following syntaxes:+--+-- * The default syntax, which is basically defined by phrase "any part in braces is variable substitution".+--   This syntax is defined in @Data.Text.Format.Heavy.Parse.Braces@ module.+--+-- * Shell-like syntax, which is basically defined by phrase "any part starting with dollar sign is variable+--   substitution". This syntax is defined in @Data.Text.Format.Heavy.Parse.Shell@ module.+--+-- It is possible to define your own syntaxes: you just need to parse an instance of @Format@ type from some+-- sort of string. The default syntax will still remain default, in sence that @instance IsString Format@ is+-- defined in terms of this syntax in @Data.Text.Format.Heavy.Instances@ module.+--+-- Variable formats syntax depends on type of data which we are going to format. These formats can be+-- pretty complex, for example they can include alignment, rounding, and so on.+module Data.Text.Format.Heavy.Parse (+  FormatParseItem (..),++  -- * Parse functions+  parse,+  parseFormat,+  parseFormat',+  parseGenericFormat,+  parseBoolFormat,+  parseMaybeFormat,++  -- * Parsec functions+  pGenericFormat,+  pBoolFormat,+) where++import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Text.Parsec (ParseError)++import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Parse.Braces+import Data.Text.Format.Heavy.Parse.VarFormat+import Data.Text.Format.Heavy.Types++data FormatParseItem+  = FormatString TL.Text+  | FormatReplacementField TL.Text (Maybe TL.Text)+  deriving (Eq, Show)++parse :: TL.Text -> Either ParseError [FormatParseItem]+parse text = toParseItems <$> parseFormat text++toParseItems :: Format -> [FormatParseItem]+toParseItems (Format items) = map toParseItem items+ where+  toParseItem (FString text) = FormatString text+  toParseItem (FVariable name fmt) = FormatReplacementField name fmt
+ Data/Text/Format/Heavy/Parse/Braces.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module defines the default syntax of format strings, generally described as+-- "any part in braces is variable substitution".+--+-- Examples of valid variable substitutions are:+--+-- * @"Simple: {}"@+--+-- * @"Numbered: {0}"@+--+-- * @"Named: {var}"@+--+-- * @"Specifying variable formatting: {var:+8.4}"@+module Data.Text.Format.Heavy.Parse.Braces (+  -- * Parse functions+  parseFormat,+  parseFormat',++  -- * Parsec functions+  pBracesFormat,+)+where++import Data.Maybe+import qualified Data.Text as T+import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Parse.Types+import Data.Text.Format.Heavy.Types+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Text.Parsec++replaceWith :: a -> String -> Parser a+replaceWith c s = c <$ string s++unescapeBraces :: String -> String -> Parser Char+unescapeBraces open close =+  try (replaceWith '{' open)+    <|> try (replaceWith '}' close)++anyChar' :: Parser Char+anyChar' =+  unescapeBraces "{{" "}}"+    <|> unescapeBraces "\\{" "\\}"+    <|> noneOf "{}"++formatSpecChar :: Parser String+formatSpecChar = try nestedBraces <|> asString anyChar'+ where+  nestedBraces = do+    char '{'+    inner <-+      concat+        <$> many+          ( try nestedBraces+              <|> replaceWith "{{" "{"+              <|> asString (noneOf "{}")+          )+    char '}'+    return $ "{" ++ inner ++ "}"+  asString = ((: []) <$>)++pVerbatim :: Parser FormatItem+pVerbatim = (FString . TL.pack) `fmap` many1 anyChar'++pVariable :: Parser FormatItem+pVariable = do+  (name, fmt) <- between (char '{') (char '}') variable+  return $ FVariable (TL.pack name) fmt+ where+  variable = do+    name <- many $ try alphaNum <|> try (char '-') <|> char '.' <|> char '_'+    mbColon <- optionMaybe $ char ':'+    fmt <- case mbColon of+      Nothing -> return Nothing+      Just _ -> do+        fmtStr <- concat <$> many formatSpecChar+        return $ Just $ TL.pack fmtStr+    name' <-+      if null name+        then do+          st <- getState+          let n = psNextIndex st+          modifyState $ \st -> st{psNextIndex = psNextIndex st + 1}+          return $ show n+        else return name+    return (name', fmt)++-- | Parsec parser for string format.+pBracesFormat :: Parser Format+pBracesFormat = Format `fmap` many (try pVariable <|> pVerbatim)++-- | Parse string format definition.+parseFormat :: TL.Text -> Either ParseError Format+parseFormat text = runParser pBracesFormat initParserState "<format string>" text++-- | Version of parseFormat which throws @error@ in case of syntax error in the formatting string.+parseFormat' :: TL.Text -> Format+parseFormat' text = either (error . show) id $ parseFormat text
+ Data/Text/Format/Heavy/Parse/Shell.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module defines shell-like syntax of format strings, generally described as+-- "any part after dollar sign is a variable substitution".+--+-- Examples of valid variable substitutions are:+--+-- * @"Simple: ${}"@. Note that to have auto-numbered placeholders in this syntax, you have to+--   write @${}@; both dollar sign and braces are necessary.+--+-- * @"Numbered: $1"@ or @"Numbered: ${1}"@.+--+-- * @"Named: $var"@ or @"Named: ${var}"@.+--+-- * @"Specifying variable formatting: ${var:+8.4}"@. To specify variable format, you have to+--   use braces.+--+-- This syntax is not the default, so to use it you have to explicitly call @parseShellFormat'@:+--+-- @+-- {-\# LANGUAGE OverloadedStrings #\-}+-- module Main where+--+-- import Data.Time+-- import qualified Data.Text.Lazy.IO as TLIO+-- import Data.Text.Format.Heavy+-- import Data.Text.Format.Heavy.Parse.Shell+--+-- main :: IO ()+-- main = do+--   name <- getLine+--   time <- getZonedTime+--   TLIO.putStrLn $ format (parseShellFormat' "Hello, ${}! It is ${:%H:%M:%S} now.") (name, time)+-- @+module Data.Text.Format.Heavy.Parse.Shell (+  -- * Parse functions+  parseShellFormat,+  parseShellFormat',++  -- * Parsec functions+  pShellFormat,+) where++import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Text.Parsec++import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Parse.Types+import Data.Text.Format.Heavy.Types++-- TODO: proper handling of escaping+anyChar' :: Parser Char+anyChar' =+  noneOf "$" <|> try ('$' <$ string "$$")++pVerbatim :: Parser FormatItem+pVerbatim = (FString . TL.pack) `fmap` many1 anyChar'++pVariable :: Parser FormatItem+pVariable = do+  char '$'+  (name, fmt) <- try bracedVariable <|> unbracedVariable+  return $ FVariable (TL.pack name) fmt+ where+  bracedVariable = between (char '{') (char '}') $ do+    name <- many $ try alphaNum <|> try (char '-') <|> char '.'+    mbColon <- optionMaybe $ char ':'+    fmt <- case mbColon of+      Nothing -> return Nothing+      Just _ -> do+        fmtStr <- many (noneOf "}" <|> try ('}' <$ string "\\}"))+        return $ Just $ TL.pack fmtStr+    name' <-+      if null name+        then do+          st <- getState+          let n = psNextIndex st+          modifyState $ \st -> st{psNextIndex = psNextIndex st + 1}+          return $ show n+        else return name+    return (name', fmt)++  unbracedVariable = do+    name <- many1 alphaNum+    return (name, Nothing)++-- | Parsec parser for string format.+pShellFormat :: Parser Format+pShellFormat = Format `fmap` many (try pVariable <|> pVerbatim)++-- | Parse string format definition.+parseShellFormat :: TL.Text -> Either ParseError Format+parseShellFormat text = runParser pShellFormat initParserState "<format string>" text++-- | Version of parseShellFormat which throws @error@ in case of syntax error in the formatting string.+parseShellFormat' :: TL.Text -> Format+parseShellFormat' text = either (error . show) id $ parseShellFormat text
+ Data/Text/Format/Heavy/Parse/Types.hs view
@@ -0,0 +1,19 @@+module Data.Text.Format.Heavy.Parse.Types (+  -- * Utility types+  Parser,+  ParserState (..),+  initParserState,+) where++import qualified Data.Text.Lazy as TL+import Text.Parsec++data ParserState = ParserState+  { psNextIndex :: Int+  }+  deriving (Eq, Show)++initParserState :: ParserState+initParserState = ParserState 0++type Parser a = Parsec TL.Text ParserState a
+ Data/Text/Format/Heavy/Parse/VarFormat.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Text.Format.Heavy.Parse.VarFormat+where++import Control.Applicative ((<|>))+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Text.Parsec hiding ((<|>))++import Data.Text.Format.Heavy.Formats+import Data.Text.Format.Heavy.Types++-- | Parsec parser for generic (Python-like) variable format.+pGenericFormat :: Parsec TL.Text st GenericFormat+pGenericFormat = do+  mbFillAlign <- optionMaybe (try pFillAlign <?> "fill and align specification")+  let fill = fromMaybe ' ' $ fst `fmap` mbFillAlign+  let align = snd `fmap` mbFillAlign+  mbSign <- optionMaybe (pSign <?> "sign specification")+  let sign = fromMaybe OnlyNegative mbSign+  mbLeading0x <- optionMaybe (pLeading0x <?> "leading 0x specification")+  let leading0x = fromMaybe False mbLeading0x+  mbWidth <- optionMaybe (pWidth <?> "width specification")+  mbPrecision <- optionMaybe (pPrecision <?> "precision specification")+  mbRadixConvert <- optionMaybe (pRadix <?> "radix specification")+  mbConvert <- optionMaybe (pConvert <?> "conversion specification")+  return $+    GenericFormat+      { gfFillChar = fill+      , gfAlign = align+      , gfSign = sign+      , gfLeading0x = leading0x+      , gfWidth = mbWidth+      , gfPrecision = mbPrecision+      , gfRadix = fst <$> mbRadixConvert+      , gfConvert = mbConvert <|> fmap snd mbRadixConvert+      }+ where+  pAlign :: Parsec TL.Text st Align+  pAlign = do+    alignChar <- oneOf "<>^"+    align <- case alignChar of+      '<' -> return AlignLeft+      '>' -> return AlignRight+      '^' -> return AlignCenter+      _ -> fail $ "Unexpected align char: " ++ [alignChar]+    return align++  pAlignWithFill :: Parsec TL.Text st (Char, Align)+  pAlignWithFill = do+    fill <- noneOf "<>=^"+    align <- pAlign+    return (fill, align)++  pAlignWithoutFill :: Parsec TL.Text st (Char, Align)+  pAlignWithoutFill = do+    align <- pAlign+    return (' ', align)++  pFillAlign :: Parsec TL.Text st (Char, Align)+  pFillAlign = do+    try pAlignWithoutFill <|> pAlignWithFill++  pSign :: Parsec TL.Text st Sign+  pSign = do+    signChar <- oneOf "+- "+    sign <- case signChar of+      '+' -> return Always+      '-' -> return OnlyNegative+      ' ' -> return SpaceForPositive+      _ -> fail $ "Unexpected sign char: " ++ [signChar]+    return sign++  pLeading0x :: Parsec TL.Text st Bool+  pLeading0x = do+    mbSharp <- optionMaybe $ char '#'+    case mbSharp of+      Nothing -> return False+      Just _ -> return True++  natural :: Parsec TL.Text st Int+  natural = do+    ws <- many1 $ oneOf "0123456789"+    return $ read ws++  pWidth :: Parsec TL.Text st Int+  pWidth = natural++  pPrecision :: Parsec TL.Text st Int+  pPrecision = do+    char '.'+    natural++  pRadix :: Parsec TL.Text st (Radix, Conversion)+  pRadix = do+    rc <- oneOf "xXhHd"+    case rc of+      'x' -> return (Hexadecimal, LowerCase)+      'X' -> return (Hexadecimal, UpperCase)+      'h' -> return (Hexadecimal, LowerCase)+      'H' -> return (Hexadecimal, UpperCase)+      'd' -> return (Decimal, LowerCase)++  pConvert :: Parsec TL.Text st Conversion+  pConvert = do+    char '~'+    conv <- oneOf "ult"+    case conv of+      'u' -> return UpperCase+      'l' -> return LowerCase+      't' -> return TitleCase++-- | Parse generic variable format.+--+-- Syntax is:+--+-- @+-- [[fill]align][sign][#][width][.precision][radix][~conversion]+-- @+--+-- where:+--+-- * fill - padding character (space by default)+-- * align - alignment indicator (@<@, @>@, or @^@)+-- * sign - when to show number's sign (@+@, @-@, or space)+-- * @#@ - if specified, then for hexadecimal numbers the leading @0x@ will be added+-- * width - minimum length of the field+-- * precision - number of decimal places after point, for floatting-point numbers+-- * radix - @h@ or @x@ for hexadecimal, @d@ for decimal (default).+-- * conversion - text conversion symbol. Supported are: @u@ - convert to upper case,+--   @l@ - convert to lower case, @t@ - convert to title case (capitalize all words).+parseGenericFormat :: TL.Text -> Either ParseError GenericFormat+parseGenericFormat text = runParser pGenericFormat () "<variable format specification>" text++-- | Parsec parser for Bool format+pBoolFormat :: Parsec TL.Text st BoolFormat+pBoolFormat = do+  true <- many $ noneOf ":,;"+  oneOf ":,;"+  false <- many $ anyChar+  return $ BoolFormat (TL.pack true) (TL.pack false)++-- | Parse Bool format.+--+-- Syntax is:+--+-- @+-- TRUE:FALSE+-- @+--+-- Colon can be replaced with comma or semicolon.+--+-- For example, valid format specifications are @true:false@ (the default one),+-- @True:False@, @yes:no@, and so on.+parseBoolFormat :: TL.Text -> Either ParseError BoolFormat+parseBoolFormat text = runParser pBoolFormat () "<boolean format specification>" text++-- | Try to parse format for @Maybe x@ type.+-- The syntax is:+--+-- @+-- someformat|nothing+-- @+--+-- where @someformat@ is format for the @x@ type, and @nothing@ is the string+-- to be substituted for @Nothing@ value.+--+-- Returns Nothing, if format does not contain @|@. Otherwise, returns+-- @Just (someformat, nothing)@.+parseMaybeFormat :: TL.Text -> Maybe (TL.Text, TL.Text)+parseMaybeFormat text =+  let (xFmtStr, nothingStr) = TL.breakOnEnd "|" text+   in if TL.null xFmtStr+        then Nothing+        else Just (TL.init xFmtStr, nothingStr)
+ Data/Text/Format/Heavy/Time.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE UndecidableInstances #-}++-- | This module contains Formatable instances for time/date values,+-- which use Data.Time.Format notation for formats (like @%H:%M@).+-- Default date/time format is RFC 822.+--+-- This module is not re-exported by Data.Text.Format.Heavy by default,+-- because it defines only one of possible time formatting strings syntaxes.+-- One may like other syntax for some reason; if we re-exported this module by+-- default, it would be impossible to hide these instances to implement other.+module Data.Text.Format.Heavy.Time where++import Data.Char+import Data.Default+import Data.String+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Data.Time+import Data.Time.Format++import Data.Text.Format.Heavy.Build+import Data.Text.Format.Heavy.Parse+import Data.Text.Format.Heavy.Types++-- | Generic time formatter, using Data.Time.Format+genericTimeFormat :: (FormatTime t) => VarFormat -> t -> Either String B.Builder+genericTimeFormat Nothing x = Right $ B.fromString $ formatTime defaultTimeLocale rfc822DateFormat x+genericTimeFormat (Just fmtStr) x =+  Right $ B.fromString $ formatTime defaultTimeLocale (TL.unpack fmtStr) x++------------------------ Formatable instances -------------------------------------------++-- instance Formatable UniversalTime where+--   formatVar fmt x = genericTimeFormat fmt x++instance Formatable Day where+  formatVar fmt x = genericTimeFormat fmt x++instance Formatable UTCTime where+  formatVar fmt x = genericTimeFormat fmt x++instance Formatable TimeZone where+  formatVar fmt x = genericTimeFormat fmt x++instance Formatable TimeOfDay where+  formatVar fmt x = genericTimeFormat fmt x++instance Formatable LocalTime where+  formatVar fmt x = genericTimeFormat fmt x++instance Formatable ZonedTime where+  formatVar fmt x = genericTimeFormat fmt x
+ Data/Text/Format/Heavy/Types.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}++-- | This module contains basic type definitions+module Data.Text.Format.Heavy.Types where++import Data.Default++#if MIN_VERSION_base(4,9,0)+import Data.Monoid (Monoid)+import Data.Semigroup ((<>))+import qualified Data.Semigroup as Semigroup+#else+import Data.Monoid+#endif++import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B++-- | Variable name+type VarName = TL.Text++-- | Variable format in text form. Nothing means default format.+type VarFormat = Maybe TL.Text++-- | String format item.+data FormatItem+  = -- | Verbatim text+    FString TL.Text+  | FVariable+      { vName :: VarName+      -- ^ Variable name+      , vFormat :: VarFormat+      -- ^ Variable format+      }+  deriving (Eq)++instance Show FormatItem where+  show (FString text) = TL.unpack text+  show (FVariable name Nothing) = TL.unpack $ "{" <> name <> "}"+  show (FVariable name (Just fmt)) = TL.unpack $ "{" <> name <> ":" <> fmt <> "}"++-- | String format+data Format = Format [FormatItem]+  deriving (Eq)++instance Show Format where+  show (Format lst) = concat $ map show lst++appendFormat :: Format -> Format -> Format+appendFormat (Format xs) (Format ys) = Format (xs ++ ys)++#if MIN_VERSION_base(4,9,0)+instance Semigroup.Semigroup Format where+  (<>) = appendFormat+#endif++instance Monoid Format where+  mempty = Format []++#if MIN_VERSION_base(4,11,0)+  -- starting with base-4.11, mappend definitions are redundant;+#elif MIN_VERSION_base(4,9,0)+  -- this is redundant starting with base-4.11 / GHC 8.4+  mappend = (Semigroup.<>)+#else+  -- prior to GHC 8.0 / base-4.9 where no `Semigroup` class existed+  mappend = appendFormat+#endif++-- | Can be used for different data types describing formats of specific types.+class (Default f, Show f) => IsVarFormat f where+  -- | Left for errors.+  parseVarFormat :: TL.Text -> Either String f++instance IsVarFormat () where+  parseVarFormat "" = Right ()+  parseVarFormat fmt = Left $ "Unsupported format: " ++ TL.unpack fmt++-- | Value that can be formatted to be substituted into format string.+class Formatable a where+  -- | Format variable according to format specification.+  -- This function should usually parse format specification by itself.+  formatVar+    :: VarFormat+    -- ^ Variable format specification in text form. Nothing is for default format.+    -> a+    -- ^ Variable value.+    -> Either String B.Builder+    -- ^ Left for errors in variable format syntax, or errors during formatting.++-- | Any variable that can be substituted.+-- This type may be also used to construct heterogeneous lists:+-- @[Variable 1, Variable "x"] :: [Variable]@.+data Variable = forall a. (Formatable a) => Variable a++instance Show Variable where+  show (Variable v) = either error toString $ formatVar Nothing v+   where+    toString :: B.Builder -> String+    toString b = TL.unpack $ B.toLazyText b++instance Formatable Variable where+  formatVar fmt (Variable x) = formatVar fmt x++-- | Format one variable according to format specification.+formatAnyVar :: VarFormat -> Variable -> Either String B.Builder+formatAnyVar fmt (Variable v) = formatVar fmt v++-- | Data structure that contains some number of variables.+class VarContainer c where+  lookupVar :: VarName -> c -> Maybe Variable++class (VarContainer c) => ClosedVarContainer c where+  allVarNames :: c -> [VarName]++------------------------------------------------------------------------------
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017, Ilya Portnov++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Ilya Portnov nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,179 @@+# format-heavy++`format-heavy` is a continuation of [`text-format-heavy`](https://github.com/portnov/text-format-heavy),+originally created by Ilya Portnov, and provides Haskell string formatting inspired by Python's `str.format()` syntax.++It supports positional and named placeholders, per-value format specs, custom+variable containers, and custom `Formatable` instances.++## Quick Start++```haskell+{-# LANGUAGE OverloadedStrings #-}++import Data.Text.Format.Heavy+import qualified Data.Text.Lazy as TL++hello :: TL.Text+hello = format "Hello, {name}!" [("name", "world" :: TL.Text)]+```++## Placeholder Syntax++```haskell+format "{}" (Single ("hello" :: TL.Text))+-- "hello"++format "{0} {1}" ("hello" :: TL.Text, "world" :: TL.Text)+-- "hello world"++format "{name}" [("name", "world" :: TL.Text)]+-- "world"++format "{{name}}" ()+-- "{name}"+```++The default syntax uses braces. Literal braces can be escaped as `{{` and `}}`.++## Passing Variables++Use `Single` for one value:++```haskell+format "value: {}" (Single (42 :: Int))+-- "value: 42"+```++Use tuples or lists for positional placeholders:++```haskell+format "{0}, {1}" ("hello" :: TL.Text, "world" :: TL.Text)+-- "hello, world"++format "{0}, {2}" (Several ["zero", "one", "two" :: TL.Text])+-- "zero, two"+```++Use association lists or maps for named placeholders:++```haskell+format "Hello, {name}!" [("name", "Alice" :: TL.Text)]+-- "Hello, Alice!"+```++For heterogeneous named values, wrap each value in `Variable`:++```haskell+let vars =+      [ ("name", Variable ("Alice" :: TL.Text))+      , ("count", Variable (3 :: Int))+      ]++format "{name} has {count} messages" vars+-- "Alice has 3 messages"+```++## Format Specs++Format specs are written after `:` and interpreted by the value being formatted:++```haskell+format "hex: {:#x}" (Single (427 :: Int))+-- "hex: 0x1ab"++format "float: {:+6.4}" (Single (2.718281828 :: Double))+-- "float: +2.7183"++format "center: <{:^10}>" (Single ("hello" :: String))+-- "center: <   hello  >"++format "upper: {:~u}" (Single ("hello" :: TL.Text))+-- "upper: HELLO"++format "bool: {:yes:no}" (Single False)+-- "bool: no"+```++`Maybe` values can specify a fallback after `|`:++```haskell+format "value: {:.3|<missing>}" (Single (Nothing :: Maybe Float))+-- "value: <missing>"+```++Any value with a `Show` instance can be formatted through `Shown`:++```haskell+format "debug: {}" (Single (Shown (Just True)))+-- "debug: Just True"+```++## Error Handling++`format` is convenient and throws an error if formatting fails.++Use `formatEither` when errors should be handled explicitly:++```haskell+import Data.Text.Format.Heavy.Build (formatEither)++formatEither "missing: {1}" (Single ("value" :: TL.Text))+-- Left "Parameter not found: 1"+```++## Parsing And Introspection++Use `Data.Text.Format.Heavy.Parse.parse` to inspect a format string without+formatting it:++```haskell+import qualified Data.Text.Format.Heavy.Parse as Format++Format.parse "{name}"+-- Right [FormatReplacementField "name" Nothing]++Format.parse "{name:.2}"+-- Right [FormatReplacementField "name" (Just ".2")]++Format.parse "{{name}} {name}"+-- Right [FormatString "{name} ", FormatReplacementField "name" Nothing]++Format.parse "{value:{width}}"+-- Right [FormatReplacementField "value" (Just "{width}")]+```++This API exposes the field name and the raw format spec parsed from the default+braces syntax. It does not format values.++## Shell-Like Syntax++The default syntax uses braces. A shell-like syntax is also available:++```haskell+import Data.Text.Format.Heavy+import Data.Text.Format.Heavy.Parse.Shell+import qualified Data.Text.Lazy as TL++format (parseShellFormat' "Hello, $name!") [("name", "world" :: TL.Text)]+-- "Hello, world!"+```++Braced shell-style placeholders can also carry format specs:++```haskell+format (parseShellFormat' "hex: ${:#x}") (Single (427 :: Int))+-- "hex: 0x1ab"+```++## Extending++Applications can extend the library by defining:++- `Formatable` instances for custom value types.+- `VarContainer` instances for custom variable sources.+- Custom parsers that produce `Format`.++## License++BSD-3-Clause. See `LICENSE`.
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ examples/Benchmarks.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}++import Criterion.Main+import qualified Data.Text as T+import Data.Text.Format.Heavy+import qualified Data.Text.Lazy as L+import qualified Text.Printf as P++printf1 :: (P.PrintfArg a) => String -> a -> String+printf1 f a = P.printf f a++printf2 :: (P.PrintfArg a, P.PrintfArg b) => String -> (a, b) -> String+printf2 f (a, b) = P.printf f a b++printf3+  :: (P.PrintfArg a, P.PrintfArg b, P.PrintfArg c)+  => String -> (a, b, c) -> String+printf3 f (a, b, c) = P.printf f a b c++main =+  defaultMain+    [ bgroup+        "arity"+        [ bench "0" $ nf (format "hi") ()+        , bench "1" $ nf (format "hi {}") (Single $ T.pack "mom")+        , bench "2" $+            nf+              (format "hi {}, how are {}")+              (T.pack "mom", T.pack "you")+        , bench "3" $+            nf+              (format "hi {}, how are {} keeping {}")+              (T.pack "mom", T.pack "you", T.pack "now")+        , bench "4" $+            nf+              (format "hi {}, {} - how are {} keeping {}")+              (T.pack "mom", T.pack "hey", T.pack "you", T.pack "now")+        ]+    , bgroup+        "comparison"+        [ bench "format1" $ nf (format "hi mom {}\n") (Single (pi :: Double))+        , bench "printf1" $ nf (printf1 "hi mom %f\n") (pi :: Double)+        , bench "show1" $ nf (\d -> "hi mom " ++ show d ++ "\n") (pi :: Double)+        , bench "format2" $ nf (format "hi mom {} {}\n") (pi :: Double, "yeah" :: T.Text)+        , bench "printf2" $ nf (printf2 "hi mom %f %s\n") (pi :: Double, "yeah" :: String)+        , bench "show2" $+            nf (\(d, s) -> "hi mom " ++ show d ++ " " ++ show s ++ "\n") (pi :: Double, "yeah" :: String)+        , bench "format3" $ nf (format "hi mom {} {} {}\n") (pi :: Double, "yeah" :: T.Text, 21212121 :: Int)+        , bench "printf3" $ nf (printf3 "hi mom %f %s %d\n") (pi :: Double, "yeah" :: String, 21212121 :: Int)+        , bench "show3" $+            nf+              (\(d, s, i) -> "hi mom " ++ show d ++ " " ++ show s ++ "\n")+              (pi :: Double, "yeah" :: String, 21212121 :: Int)+        ]+    , bgroup+        "types"+        [ bench "unit" $ nf (format "hi") ()+        , bgroup+            "int"+            [ bench "small" $ nf (format "hi {}") (Single (1 :: Int))+            , bench "medium" $ nf (format "hi {}") (Single (1234 :: Int))+            , bench "large" $ nf (format "hi {}") (Single (0x7fffffff :: Int))+            ]+        , bgroup+            "float"+            [ bench "small" $ nf (format "hi {}") (Single (1 :: Float))+            , bench "medium" $ nf (format "hi {}") (Single (pi :: Float))+            , bench "large" $ nf (format "hi {}") (Single (pi * 1e37 :: Float))+            ]+        , bgroup+            "double"+            [ bench "small" $ nf (format "hi {}") (Single (1 :: Double))+            , bench "medium" $ nf (format "hi {}") (Single (pi :: Double))+            , bench "large" $ nf (format "hi {}") (Single (pi * 1e37 :: Double))+            ]+        , bgroup+            "string"+            [ bench "small" $ nf (format "hi {}") (Single ("mom" :: String))+            , bench "medium" $+                nf+                  (format "hi {}")+                  (Single . concat . replicate 64 $ ("mom" :: String))+            , bench "large" $+                nf+                  (format "hi {}")+                  (Single . concat . replicate 1024 $ ("mom" :: String))+            ]+        , bgroup+            "text"+            [ bench "small" $ nf (format "hi {}") (Single (T.pack "mom"))+            , bench "medium" $ nf (format "hi {}") (Single (T.replicate 64 "mom"))+            , bench "large" $ nf (format "hi {}") (Single (T.replicate 1024 "mom"))+            ]+        , bgroup+            "lazytext"+            [ bench "small" $ nf (format "hi {}") (Single (L.pack "mom"))+            , bench "medium" $+                nf+                  (format "hi {}")+                  (Single . L.fromChunks . replicate 64 $ "mom")+            , bench "large" $+                nf+                  (format "hi {}")+                  (Single . L.fromChunks . replicate 1024 $ "mom")+            ]+        ]+    ]
+ examples/test.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings #-}++import Data.Text.Format.Heavy+import Data.Text.Format.Heavy.Time+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.IO as TLIO+import Data.Time++main :: IO ()+main = do+  let template = "x: {:#x}, y: <{:^10}>, z: {:+6.4}, x(d): {0}, t: {}, y: {}, r: {}; bool: {:yes:no}."+      -- xs = [Variable (18 :: Int), Variable ("hello" :: String), Variable (3 :: Int)]+      rt = (Right 7) :: Either String Int+      xs =+        ( 18 :: Int+        , "hello" :: String+        , 2.718281828 :: Double+        , Shown (Just (7 :: Int))+        , Just (8 :: Int)+        , rt+        , True+        )+  TLIO.putStrLn $ format template xs+  time <- getZonedTime+  let vars :: [(TL.Text, Variable)]+      vars =+        [ ("name", Variable ("Ilya" :: String))+        , ("time", Variable time)+        , ("header.content-type", Variable ("text/json" :: String))+        , ("upper", Variable ("this will be uppercase" :: String))+        , ("lower", Variable ("This will be ALL lower case" :: String))+        , ("title", Variable ("this is the title" :: String))+        , ("noun", Variable ("string" :: String))+        ]+  TLIO.putStrLn $+    format+      "Hello, {name}! It is {time:%H:%M:%S} now. Test {noun}ification. Content: {header.content-type} x. Upper: <{upper:~u}>, lower <{lower:~l}>, title: <{title:~t}>."+      vars+  let mbX = Nothing :: Maybe Float+      mbY = Just 7.37491 :: Maybe Float+      mbZ = Nothing :: Maybe Float+  TLIO.putStrLn $+    format+      "Maybe X: {:+8.4|<not defined>}, Maybe Y: {:+8.4|<not defined>}, Maybe Z: {:+8.4}."+      (mbX, mbY, mbZ)
+ format-heavy.cabal view
@@ -0,0 +1,84 @@+cabal-version: 1.12++-- Keep this file synchronized with package.yaml when changing package+-- metadata, dependencies, exposed modules, or test configuration.++name:           format-heavy+version:        0.1.0.2+synopsis:       Full-weight string formatting library, analog of Python's string.format+description:    This package contains full-featured string formatting function, similar to Python's string.format. Features include:+                * Automatically numbered variable placeholders;+                * Positional variable placeholders;+                * Named variable placeholders;+                * Placeholders can be used in any order; one variable can be used several+                  times or not used at all.+                .+                * Specific format can be used for each variable substitution.+                This package prefers functionality over "light weight" and (probably) performance.  It also exposes all required interfaces to extend and customize it.+                See the README and @examples/@ directory for details.+category:       Text+homepage:       https://github.com/lbobylev/format-heavy#format-heavy+bug-reports:    https://github.com/lbobylev/format-heavy/issues+author:         Ilya Portnov+maintainer:     Leonid Bobylev <l3o6@proton.me>+tested-with:    GHC == 9.2.8, GHC == 9.4.8, GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    ChangeLog.md+    README.md+    examples/test.hs+    examples/Benchmarks.hs++source-repository head+  type: git+  location: https://github.com/lbobylev/format-heavy++library+  exposed-modules:+      Data.Text.Format.Heavy+      Data.Text.Format.Heavy.Build+      Data.Text.Format.Heavy.Formats+      Data.Text.Format.Heavy.Instances+      Data.Text.Format.Heavy.Parse+      Data.Text.Format.Heavy.Parse.Braces+      Data.Text.Format.Heavy.Parse.Shell+      Data.Text.Format.Heavy.Parse.Types+      Data.Text.Format.Heavy.Parse.VarFormat+      Data.Text.Format.Heavy.Time+      Data.Text.Format.Heavy.Types+  other-modules:+       Paths_format_heavy+  hs-source-dirs:+      .+  build-depends:+      base >=4.16 && <5+    , bytestring >=0.12 && <0.13+    , containers >=0.7 && <0.8+    , data-default >=0.8 && <0.9+    , labels >=0.3 && <0.4+    , parsec >=3.1.18 && <3.2+    , template-haskell >=2.18 && <2.24+    , text >=2.1 && <3+    , th-lift >=0.8 && <0.9+    , th-lift-instances >=0.1 && <0.2+    , time >=1.12 && <1.15+  default-language: Haskell2010++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+       Paths_format_heavy+  hs-source-dirs:+      tests+  build-depends:+      base >=4.16 && <5+    , bytestring >=0.12 && <0.13+    , hspec+     , format-heavy+    , time >=1.12 && <1.15+    , containers+    , text+  default-language: Haskell2010
+ tests/Spec.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE OverloadedStrings #-}++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL+import Data.Map (Map)+import qualified Data.Map as Map+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Time+import Test.Hspec++import Data.Text.Format.Heavy+import Data.Text.Format.Heavy.Build (formatEither)+import Data.Text.Format.Heavy.Parse (FormatParseItem (..), parse)+import Data.Text.Format.Heavy.Parse.Shell+import Data.Text.Format.Heavy.Time++main :: IO ()+main = hspec $ do+  describe "trivial" $ do+    it "formats string literal without formatting characters" $ do+      format "hello world" () `shouldBe` "hello world"++  describe "parse" $ do+    it "returns replacement fields" $ do+      parse "{name}"+        `shouldBe` Right [FormatReplacementField "name" Nothing]+      parse "{name:.2}"+        `shouldBe` Right [FormatReplacementField "name" (Just ".2")]++    it "keeps strings and fields in order" $ do+      parse "{{name}} {name}"+        `shouldBe` Right [FormatString "{name} ", FormatReplacementField "name" Nothing]++    it "keeps nested format specs raw" $ do+      parse "{value:{width}}"+        `shouldBe` Right [FormatReplacementField "value" (Just "{width}")]++  describe "simple" $ do+    it "formats int properly" $ do+      format "integer: {}" (Single (7 :: Int)) `shouldBe` "integer: 7"++    it "formats strings properly" $ do+      format "string: {}" (Single ("hello" :: String)) `shouldBe` "string: hello"++    it "formats text values properly" $ do+      format "strict: {}" (Single (T.pack "hello")) `shouldBe` "strict: hello"+      format "lazy: {}" (Single (TL.pack "hello")) `shouldBe` "lazy: hello"++    it "formats utf8 byte strings properly" $ do+      format "strict: {}" (Single (BS.pack [104, 101, 108, 108, 111])) `shouldBe` "strict: hello"+      format "lazy: {}" (Single (BSL.pack [104, 101, 108, 108, 111])) `shouldBe` "lazy: hello"++    it "handles parameter numbers" $ do+      format "one: {0}, two: {1}" ((1 :: Int), (2 :: Int)) `shouldBe` "one: 1, two: 2"+      format "two: {1}, one: {0}" ((1 :: Int), (2 :: Int)) `shouldBe` "two: 2, one: 1"++    describe "handles parameters names" $ do+      it "with ascii characters" $ do+        format+          "one: {theKey}!"+          ((Map.singleton "theKey" "the string") :: Map TL.Text TL.Text)+          `shouldBe` "one: the string!"+      it "with dots" $ do+        format+          "one: {the.key}!"+          ((Map.singleton "the.key" "the string") :: Map TL.Text TL.Text)+          `shouldBe` "one: the string!"+      it "with dashes" $ do+        format+          "one: {the-key}!"+          ((Map.singleton "the-key" "the string") :: Map TL.Text TL.Text)+          `shouldBe` "one: the string!"+      it "with underscores" $ do+        format+          "one: {the_key}!"+          ((Map.singleton "the_key" "the string") :: Map TL.Text TL.Text)+          `shouldBe` "one: the string!"++    it "handles additional variable containers" $ do+      format "{0}, {1}, {2}" (("one" :: String), (2 :: Int), ("three" :: String))+        `shouldBe` "one, 2, three"+      format "{0}, {2}" (Several ["zero", "one", "two" :: String])+        `shouldBe` "zero, two"+      format "hello {name}" ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "hello world"++    it "handles defaulting containers" $ do+      format+        "present: {0}; missing: {1}"+        (Single ("value" :: String) `withDefault` Variable ("fallback" :: TL.Text))+        `shouldBe` "present: value; missing: fallback"+      format "present: {0}; missing: {1}" (optional (Single ("value" :: String)))+        `shouldBe` "present: value; missing: "++    it "handles escaped braces" $ do+      format "{{}}" () `shouldBe` "{}"+      format "\\{name\\}" () `shouldBe` "{name}"+      format "{{{name}}}" ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "{world}"+      format+        "json: {{\"answer\": \"{answer}\"}}"+        ([("answer", "ok" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "json: {\"answer\": \"ok\"}"++  describe "documentation" $ do+    it "formats examples from wiki" $ do+      format "hex: {:#x}" (Single (427 :: Int)) `shouldBe` "hex: 0x1ab"+      format "hex: {:#h}" (Single (427 :: Int)) `shouldBe` "hex: 0x1ab"+      format "hex: {:#X}" (Single (427 :: Int)) `shouldBe` "hex: 0x1AB"+      format "hex: {:#H}" (Single (427 :: Int)) `shouldBe` "hex: 0x1AB"+      format "dec: {:#d}" (Single (17 :: Int)) `shouldBe` "dec: 17"+      format "center: <{0:^10}>" (Single ("hello" :: String)) `shouldBe` "center: <   hello  >"+      format "float: {:+6.4}" (Single (2.718281828 :: Double)) `shouldBe` "float: +2.7183"++    it "formats alignment and text conversion options" $ do+      format "left: <{:<8}>" (Single ("hi" :: String)) `shouldBe` "left: <hi      >"+      format "right: <{:>8}>" (Single ("hi" :: String)) `shouldBe` "right: <      hi>"+      format "center: <{:^7}>" (Single ("hi" :: String)) `shouldBe` "center: <   hi  >"+      format "fill: <{:*>8}>" (Single ("hi" :: String)) `shouldBe` "fill: <******hi>"+      format "upper: {:~u}" (Single ("hello" :: String)) `shouldBe` "upper: HELLO"+      format "lower: {:~l}" (Single ("HELLO" :: String)) `shouldBe` "lower: hello"+      format "title: {:~t}" (Single ("hello world" :: String)) `shouldBe` "title: Hello World"++    it "formats numeric options" $ do+      format "positive: {:+d}" (Single (7 :: Int)) `shouldBe` "positive: +7"+      format "negative: {:+d}" (Single (-7 :: Int)) `shouldBe` "negative: -7"+      format "space: {: d}" (Single (7 :: Int)) `shouldBe` "space:  7"+      format "hex: {:#X}" (Single (255 :: Integer)) `shouldBe` "hex: 0xFF"++    it "formats booleans" $ do+      format "default: {}" (Single True) `shouldBe` "default: true"+      format "enable: {:yes:no}" (Single False) `shouldBe` "enable: no"++    it "formats maybes" $ do+      format "Value: {:.3|<undefined>}." (Single (2.718281828 :: Float)) `shouldBe` "Value: 2.718."+      format "Value: {:.3|<undefined>}." (Single (Nothing :: Maybe Float))+        `shouldBe` "Value: <undefined>."+      format "Value: {:.3}." (Single (Nothing :: Maybe Float)) `shouldBe` "Value: ."++    it "formats either and shown values" $ do+      format "left: {}" (Single (Left ("error" :: String) :: Either String Int)) `shouldBe` "left: error"+      format "right: {}" (Single (Right (7 :: Int) :: Either String Int)) `shouldBe` "right: 7"+      format "shown: {}" (Single (Shown (True, False))) `shouldBe` "shown: (True,False)"++    it "formats time" $ do+      let yektLocale =+            defaultTimeLocale+              { knownTimeZones = [TimeZone (5 * 60) False "YEKT"]+              }+          -- `defaultTimeLocale` does not know about Yekaterinburg.+          Just time = parseTimeM True yektLocale rfc822DateFormat "Sat,  3 Jun 2017 19:06:01 YEKT" :: Maybe ZonedTime+      format "time: {:%H:%M:%S}" (Single time) `shouldBe` "time: 19:06:01"+      format "time: {:%H:%M:%S %Z}" (Single time) `shouldBe` "time: 19:06:01 YEKT"+      format "default: {}" (Single time) `shouldBe` "default: Sat,  3 Jun 2017 19:06:01 YEKT"++  describe "errors" $ do+    it "reports missing parameters" $ do+      formatEither "missing: {1}" (Single ("value" :: String))+        `shouldBe` Left "Parameter not found: 1"+      formatEither "missing: {2}" (Several ["zero", "one" :: String])+        `shouldBe` Left "Parameter not found: 2"++  describe "shell syntax" $ do+    it "formats shell-style substitutions" $ do+      format (parseShellFormat' "hello $name") ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "hello world"+      format+        (parseShellFormat' "hello ${name}")+        ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "hello world"+      format (parseShellFormat' "${} ${}") (("one" :: String), ("two" :: String))+        `shouldBe` "one two"+      format+        (parseShellFormat' "cost: $$${amount}")+        ([("amount", "10" :: TL.Text)] :: [(TL.Text, TL.Text)])+        `shouldBe` "cost: $10"