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 +13/−0
- Data/Text/Format/Heavy.hs +39/−0
- Data/Text/Format/Heavy/Build.hs +132/−0
- Data/Text/Format/Heavy/Formats.hs +63/−0
- Data/Text/Format/Heavy/Instances.hs +492/−0
- Data/Text/Format/Heavy/Parse.hs +64/−0
- Data/Text/Format/Heavy/Parse/Braces.hs +100/−0
- Data/Text/Format/Heavy/Parse/Shell.hs +100/−0
- Data/Text/Format/Heavy/Parse/Types.hs +19/−0
- Data/Text/Format/Heavy/Parse/VarFormat.hs +178/−0
- Data/Text/Format/Heavy/Time.hs +55/−0
- Data/Text/Format/Heavy/Types.hs +121/−0
- LICENSE +30/−0
- README.md +179/−0
- Setup.hs +3/−0
- examples/Benchmarks.hs +107/−0
- examples/test.hs +45/−0
- format-heavy.cabal +84/−0
- tests/Spec.hs +177/−0
+ 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"