packages feed

ginsu-0.8.0.2: Format.hs

-- Copyright (c) 2002 John Meacham (john@foo.net)
--
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


-- | General text formatting routines, similar to C printf-style formatting.
--
-- > General pattern format
-- > '%' '%'                                             output literal '%'
-- > '%' flags width '.' precision '\'' string '\''      always print string as is.
-- > '%' flags width '.' precision formatChar            passed to class instance.
-- > '%' '(' sub1, sub2, ... ')' flags width '.' precision formatChar  subpatterns
--
-- > Flags is zero or more of the following
-- > ' ' a blank should be left before positive numbers
-- > '#' value should be printed in alternate form
-- > '0' value should be zero padded
-- > '-' value should be left adjusted
-- > '+' value should always have a sign
-- > '\'' numerical values should be grouped
--
-- > formatChar is a character affecting formatting
-- > '/' always format as if by the following translation
-- >     fmt "%fx.yv" [fa x] -> fmt "%fx.ys" [fa (show x)]
-- > 'l' format as list
-- > 's' print list compactly, similar to %(%c)l
-- > 'x' hexadecimal
-- > 'i' integral
-- > 'b' binary
-- > 'o' octal
-- > 'c' print compactly, as is used in 's'


module Format(
    -- * The Format class
    Format(..),
    -- * Functions
    -- ** Formatting routines
    fmt, fmtS, fmtSs, errorf,
    -- ** Arguments
    fa, fS, fi, fs, ff,
    -- * Internals, useful for creating instances of Format
    Pattern(..),
    formatIntegral, formatString, formatShow,
    testFormat
    ) where

import Data.Char(isDigit,ord)
import Data.List(intersperse)

-- | the type of patterns
data Pattern = Pattern {
    patternSub :: [String], -- ^ subpatterns
    patternFlags :: [Char],
    patternWidth :: Maybe Int,
    patternPrecision :: Maybe Int,
    patternChar :: Char
    }

pattern :: Pattern
pattern = Pattern { patternSub = [], patternFlags = [],
    patternWidth = Nothing, patternPrecision = Nothing,
    patternChar = '/' }




type F = Pattern -> String

-- | class for use with 'fa'
class Show a => Format a where
    format :: a -> Pattern -> String
    format = formatShow

-- | format anything
fa :: (Format a) => a -> F
fa x p | patternChar p == '/' = formatShow x p
fa x p = format x p
-- | format showable
fS :: (Show a) => a -> F
fS x = fs (show x)
-- | format integer. specialization of 'fa', useful to resolve ambiguity.
fi :: Int -> F
fi = fa
-- | format string. specialization of 'fa', useful to resolve ambiguity.
fs :: String -> F
fs = fa
-- | format float. specialization of 'fa', useful to resolve ambiguity.
ff :: Float -> F
ff = fa



-- | main formatting routine.
-- @fmt \"%s has %i pets\" [fa \"John\", fi 3]@

fmt :: String -> [F] -> String
fmt ('%':'%':xs) fs = '%':fmt xs fs
fmt ('%':xs) (f:fs) =  case xs5 of
	('\'':cs) -> let (a,b) = gs "" cs in formatString a bp ++ fmt b fs
	(c:cs) -> f (bp {patternChar = c}) ++ fmt cs fs
	[] -> []
     where
    bp = pattern {patternSub = ss, patternFlags = flags, patternWidth = w, patternPrecision =  p}
    (flags,xs2) = span (`elem` "# 0-+'") xs
    (w,xs3) = grabNum xs2
    (p,xs4) = case xs3 of
	('.':xs) -> grabNum xs
	xs -> (Nothing, xs)
    (ss,xs5) = case xs4 of
	('(':xs) -> let (u,v) = span (/= ')') xs in ([u],tail v)
	xs -> ([],xs)
    grabNum xs = let (u,v) = span isDigit xs in if null u then (Nothing,v) else (Just (read u),v)
    gs x ('\'':'\'':cs) = gs ('\'':x) cs
    gs x ('\'':cs) = (reverse x,cs)
    gs x (c:cs) = gs (c:x) cs
    gs x [] = (reverse x,[])
fmt (x:xs) fs = x:fmt xs fs
fmt "" _ = ""

-- | format a single string
fmtS :: String -> String -> String
fmtS f x = fmt f [fs x]

-- | format a set of strings
fmtSs :: String -> [String] -> String
fmtSs f xs = fmt f (map fs xs)


-- | throws an error created by format
errorf :: String -> [F] -> a
errorf s fs = error $ fmt s fs

putBase :: Int -> Integer -> String
putBase base x = if null v then "0" else v where
    v = reverse (foo x)
    hex = "0123456789abcdef"
    foo 0 = ""
    foo x = let (u,v) = x `divMod` (toInteger base) in
	(hex !! fromIntegral v) : foo u


childFmt :: Pattern -> [F] -> String
childFmt (Pattern {patternSub = [s]})  = fmt s
childFmt _ = fmt "%/"

instance Format Float
instance Format ()
instance Format Int where format = formatIntegral
instance Format Integer where format = formatIntegral
instance (Format a, Format b) => Format (a,b) where
    format (x,y) p | patternChar p == 't' = format x p ++ format y p
    format x p = formatShow x p
instance Format a => Format [a] where
    format xs p | ( '#' `elem` patternFlags p) && patternChar p == 's' = "\""  ++ formatString (concatMap (flip format $ pattern {patternChar = 'c'}) xs) p ++ "\""
    format xs p | patternChar p == 's' = formatString ( concatMap (flip format $ pattern {patternChar = 'c'}) xs ) p
    format xs p | ( '#' `elem` patternFlags p) && patternChar p == 'l' = "[" ++ concat (intersperse ", " (map (\x -> childFmt p [fa x]) xs)) ++ "]"
    format xs p | patternChar p == 'l' = ( concat (map (\x -> childFmt p [fa x]) xs))
    format xs p = formatShow xs p

instance Format a => Format (Maybe a) where
    format (Just x) p = childFmt p [fa x]
    format  Nothing p = "<Nothing>"
instance Format Char where
    format x p | patternChar p == 'c' = x:[]
    format x p | patternChar p `elem` integralChars = formatIntegral (ord x) p
    format x p = formatShow x p
    --format x _  = show x

instance (Format a,Format b) => Format (Either a b) where
    format (Left x) p | ( '#' `elem` patternFlags p)  = "Left " ++ childFmt p [fa x]
    format (Right x) p | ( '#' `elem` patternFlags p)  = "Right " ++ childFmt p [fa x]
    format (Left x) p = childFmt p [fa x]
    format (Right x) p = childFmt p [fa x]

instance Format Bool where
    format True p | patternChar p == 'b' = "true"
    format False p | patternChar p == 'b' = "false"
    format v p  = formatShow v p


formatShow :: Show a => a -> Pattern -> String
formatShow x = formatString (show x)

integralChars = "xboiu"

formatIntegral :: (Integral n, Show n) => n -> Pattern -> String
formatIntegral v p@(Pattern {patternChar = 'x'}) = integralFmt p v 16
formatIntegral v p@(Pattern {patternChar = 'b'}) = integralFmt p v 2
formatIntegral v p@(Pattern {patternChar = 'o'}) = integralFmt p v 8
formatIntegral v p@(Pattern {patternChar = 'i'}) = integralFmt p v 10
formatIntegral v p@(Pattern {patternChar = 'u'}) = integralFmt p v 10
formatIntegral v p = formatShow v p

integralFmt p v base = pre ++ val where
    pos = if ' ' `elem` patternFlags p then " " else if '+' `elem` patternFlags p then "+" else ""
    pre = if v < 0 then "-" else pos
    val = putBase base (abs $ toInteger v)

formatString ::  String -> Pattern -> String
formatString s p@(Pattern {patternPrecision = Just x }) = formatString (take x s) (p {patternPrecision = Nothing})
formatString s (Pattern {patternWidth = Just w }) | length s >= w = s
formatString s (Pattern {patternFlags = pf, patternWidth = Just w} ) = if '-' `elem` pf then s ++ e else e ++ s where
    e = replicate (w - length s) ' '
formatString s _ = s


{-
fmtStr :: Bool -> Int -> String -> String
fmtStr _ 0 s = s
fmtStr False n s = take (abs n) s
fmtStr True n s | length s > (abs n) = take (abs n) s
fmtStr True n s | n > 0 =  replicate (n - length s) ' ' ++ s
fmtStr True n s | n < 0 = s ++ replicate ((negate n) - length s) ' '
-}


------------
-- test case
------------

concatInter p xs = concat (intersperse p xs)

nums :: [Integer]
nums = [ 0,  878987, -2830,  77, 0xabcdef]

twords = ["foo", "a", "fuzz"]

fmts = ["%8i", "%-+8i", "%b", "%o", "%x"]


tfmt :: String -> [F] -> String
tfmt f v = show (fmt f v) ++ "\t\t<- fmt \"" ++ f ++ "\" [" ++ fmt (concatInter ", " (replicate (length v) "%v")) v ++ "]" where
    fmtEsc ('%':xs) = '%':'%':fmtEsc xs
    fmtEsc (x:xs) = x:fmtEsc xs
    fmtEsc "" = ""
fmtsAp x = tfmt (unwords fmts) (replicate (length fmts) (fa x)) where
    --v = map (\(fe, f) -> "(" ++ fe ++ " " ++ f ++ ")") $ zip (map fmtEsc fmts) fmts
    fmtEsc ('%':xs) = '%':'%':fmtEsc xs
    fmtEsc (x:xs) = x:fmtEsc xs
    fmtEsc "" = ""


testFormat = do
    putStr $ unlines (map fmtsAp nums)
    --putStr $ unlines $ map (\x -> fmt "*%s*" [fa x]) [(f,fmt f [fs x]) | f <- ["%8s","%-8s","%s"], x <- twords]
    putStr $ unlines $ [tfmt f [fa x] | f <- ["%8s","%-8s","%s", "%#s", "%2.2s", "%l", "%#l", "%#(chr 0x%x)l", "%(-)l"], x <- twords]