-- | -- Module : Text.PercentFormat -- Copyright : (c) 2016-2018 Rudy Matela -- License : 3-Clause BSD (see the file LICENSE) -- Maintainer : Rudy Matela -- -- The "Text.PercentFormat" library provides printf-style string formatting. -- It provides a '%' operator (as in Ruby or Python) -- and uses the old C-printf-style format you know and love. -- -- This library differs from "Text.Printf" in that it does not rely on custom -- typeclasses -- it works on anything that is a 'Show' instance that produces -- output in the supported formats. -- -- -- Formatting one value with '-%': -- -- > > "Hello %s!" -% "World" -- > "Hello World!" -- -- -- Formatting three values, tuple style, with '-%%%': -- -- > > "load average: %1.2f %1.2f %1.2f" -%%% (0.00, 0.066, 0.11) -- > "load average: 0.00 0.07 0.11" -- -- -- Formatting three values, chain style, with '%' and '-%': -- -- > > "load average: %1.2f %1.2f %1.2f" % 0.00 % 0.066 -% 0.11 -- > "load average: 0.00 0.07 0.11" -- -- -- To produce a string with a percent sign (@%@), -- use two percent signs (@%%@): -- -- > > "memory usage: %i%%" -% 13 -- > "memory usage: 13%" -- -- -- Percent signs are duplicated when using the '%' operator to allow chaining -- (further formats): -- -- > > "percent sign: %s, memory usage: %i%%" % "%" % 87 -- > "percent sign: %%, memory usage: 87%%" -- -- /Always/ use the '-%' operator when formatting the /last value/ -- to remove duplicate @%@ signs: -- -- > > "percent sign: %s, memory usage: %i%%" % "%" -% 87 -- > "percent sign: %, memory usage: 87%" -- -- To print, just prefix you format expression with "@putStrLn $@": -- -- > > putStrLn $ "Hello %s!" -% "World" -- > Hello World! -- -- -- == Supported formats -- -- * /r/ -- 'show' representation as-is (including quotes for strings). -- -- > > "%r" % "string" -- > "\"string\"" -- -- > > "%r" % Just 10 -- > "Just 10" -- -- * /s/ -- string. If the argument is 'show'ed as a 'String', intersperse it, -- otherwise include representation in whole. -- -- > > "%s" % "string" -- > "string" -- -- > > "%s" % 10 -- > "10" -- -- > > "%s" % Just "string" -- > "Just \"string\"" -- -- * /c/ -- Argument is converted to a single character. -- Accepts arguments that when 'show'ed are represented as 'Char's. -- -- > > "%c" % 'a' -- > "a" -- -- * /i/ -- Argument is converted to the nearest decimal integer. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%i" % 5040 -- > 5040 -- -- > > "%i" % 3.141 -- > 3 -- -- * /d/ -- Argument is converted to a decimal integer. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%d" % 5040 -- > 5040 -- -- > > "%i" % 3.141 -- > 3.141 -- -- * /x/ -- Argument is converted to hexadecimal format with lowercase letters. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%x" % 5040 -- > "13b0" -- -- Differently from C's @printf@, negative integers are printed prefixed with -- a minus (@-@) sign: -- -- > > "%x" % (-5040) -- > "-13b0" -- -- Differently from C's @printf@, this library is able to show hexadecimal -- fractional parts: -- -- > > "%.6x" % pi -- > "3.243f6b" -- -- * /X/ -- Argument is converted to hexadecimal format with capital letters. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%X" % 5040 -- > "13B0" -- -- * /o/ -- Argument is converted to octal format. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%o" % 5040 -- > "11660" -- -- > > "%.6o" % pi -- > "3.110376" -- -- * /b/ -- Argument is converted to binary format. -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%b" % 5040 -- > "1001110110000" -- -- > > "%.6b" % pi -- > "11.001001" -- -- * /f/ -- Argument is converted to decimal format with a fractional part -- (even when the given argument is an integer). -- Accepts arguments that when 'show'ed are represented as either -- 'Integer's, 'Rational's or 'Double's. -- -- > > "%f" % 5040 -- > "5040.0" -- -- > > "%f" % pi -- > "3.141592653589793" -- -- * /e/ -- Argument is converted to scientific notation. -- __This does not work yet. To be added in a future version.__ -- -- * /q/ -- Argument is converted to a rational number. -- __This does not work yet. To be added in a future version.__ -- -- -- == Supported flag charaters -- -- * /0/ -- the numeric value should be padded by zeros. -- -- > > "%08i" % 5040 -- > "00005040" -- -- * /-/ -- left adjusted values. -- -- > > "%-8i" % 5040 -- > "5040 " -- -- * / / -- leave a blank before a positive number. -- -- > > "% i" % 5040 -- > " 5040" -- -- > > "% i" % (-5040) -- > "-5040" -- -- * /+/ -- leave a plus sign before a positive number. -- -- > > "%+i" % 5040 -- > "+5040" -- -- > > "%+i" % (-5040) -- > "-5040" -- -- * /[1-9][0-9]*/ -- minimum field width. -- -- > > "%8i" % 5040 -- > " 5040" -- -- * /.[0-9][0-9]*/ -- precision. -- -- > > "%.2i" % 5040 -- > "5040.00" -- -- > > "%9.2i" % 5040 -- > " 5040.00" -- -- -- == How does it work? -- -- "Text.PercentFormat" works on values that are 'Show' instances producing -- results in the expected format. Take for example the following number type: -- -- > data Digit = Zero | One | Two | Three -- > instance Show Digit where -- > show Zero = "0" -- > show One = "1" -- > show Two = "2" -- > show Three = "3" -- -- "Text.PercentFormat" works fine on it: -- -- > > "%d %i %f %.2f" Zero One Two Three -- > "0 1 2 3.00" -- -- Because when 'show'ed, values of this @Digit@ type are represented as 'Integer's. -- -- -- == Error Handling -- -- This library is designed to avoid raising errors. -- If conversion cannot be performed an exclamation mark (@!@) is produced. -- If there are missing format strings an interrogation mark (@?@) is produced. -- For example: -- -- > > "%d %d" -% "Ten" -- > "! ?" -- -- The only two instances where errors are raised are: -- -- 1. the argument values contain errors themselves: -- -- > > "Hello %s!" % error "err" -- > *** Exception err -- -- > > error "err" % "World" -- > *** Exception err -- -- 2. the format string is not supported: -- -- > > "%j" % 10 -- > *** Exception: unknown format string `j' -- -- -- == Known bugs -- -- * @"%x" % 3.1415926@ takes too long to run. -- -- * @"%x" % pi@ takes /very very long/ to run. module Text.PercentFormat ( (%) , (-%) , (/%) , (%%) , (%%%) , (%%%%) , (%%%%%) , (%%%%%%) , (-%%) , (-%%%) , (-%%%%) , (-%%%%%) , (-%%%%%%) , (+%) ) where import Data.Maybe (listToMaybe, fromMaybe) import Data.Char (isDigit, toUpper) import Text.PercentFormat.Spec as S import Text.PercentFormat.Utils hiding (align) import Text.PercentFormat.Quotient (maybeReadQ, digits, Quotient, infinity, nan) import qualified Text.PercentFormat.Quotient as Q import qualified Text.PercentFormat.Utils as U import Prelude hiding (showString, showChar) -- | Formats a single value into a string without finalizing: -- leaving duplicate percent signs & remaining format sequences. -- -- > > "Hello %s!" % "World" -- > "Hello World!" -- -- > > "processor usage: %d%%" % 67 -- > "processor usage: 67%%" -- -- > > "load avg: %.2f %.2f %.2f" % 0.666 -- > "load avg: %0.67 %.2f %.2f" -- -- Please use '-%' when formatting the last value into a string so that -- duplicate percent signs are removed. (%) :: Show a => String -> a -> String ('%':s) % x = case ty sp of Percent -> '%':'%': s' % x ReprSpec -> (duplicatePercents $ showRepr sp x) ++ s' StringSpec -> (duplicatePercents $ showString sp x) ++ s' CharSpec -> (duplicatePercents $ showChar sp x) ++ s' NumberSpec -> (duplicatePercents $ showDigits sp x) ++ s' where (sp,s') = parseSpec s (c:s) % x = c : s % x "" % x = "" infixl 9 % showRepr :: Show a => Spec -> a -> String showRepr spec = align spec . show showString :: Show a => Spec -> a -> String showString spec s = case maybeRead (show s) of Nothing -> align spec (show s) Just s -> align spec s showChar :: Show a => Spec -> a -> String showChar spec c = case maybeRead (show c) of Nothing -> err '!' spec Just c -> align spec (c:"") -- TODO: refactor showDigits (currently very hacky) showDigits :: Show a => Spec -> a -> String showDigits spec x = case maybeReadQ (show x) of Nothing -> err '!' spec Just q -> applyWhen (padWith spec /= ' ') (signal q ++) . align' q . applyWhen (padWith spec == ' ') (signal q ++) . either id (\(ids,fds,pds) -> capitalize $ showds ids fds pds (precision spec)) . digits (base spec) . round' (base spec) (precision spec) $ q where capitalize = applyWhen (capitalizeDigits spec) (map toUpper) signal q | q >= 0 = positivePrefix spec | q < 0 = "-" align' :: Quotient -> String -> String align' q = if padWith spec == ' ' then align spec else align spec{width = width spec - length (signal q)} round' :: Int -> Maybe Int -> Quotient -> Quotient round' _ _ q | Q.isInfinite q = q round' _ _ q | Q.isNaN q = q round' _ Nothing q = q round' b (Just p) q = round (q * fromIntegral b ^ p) Q.% fromIntegral b ^ p showds :: [Int] -> [Int] -> [Int] -> Maybe Int -> String showds ids fds [] Nothing | length fds < minPrecision spec = showds ids (fds ++ replicate (minPrecision spec - length fds) 0) [] Nothing showds ids [] _ Nothing = intsToDigits ids showds ids fds pds (Just 0) = intsToDigits ids showds ids fds pds Nothing = intsToDigits ids ++ "." ++ intsToDigits fds ++ showPeriod pds showds ids fds pds (Just pr) = intsToDigits ids ++ "." ++ intsToDigits (take pr (fds ++ loop pds ++ repeat 0)) showPeriod [] = "" showPeriod xs = intsToDigits xs ++ intsToDigits xs ++ intsToDigits xs ++ "..." err :: Char -> Spec -> String err c spec = align spec{padWith=c} (c:"") -- | Formats the last value into a string. -- This finalizes formatting, removing duplicate percent signs and replacing -- remaining format sequences with interrogation marks. -- -- > > "Hello %s!" -% "World" -- > "Hello World!" -- -- > > "processor usage: %d%%" -% 67 -- > "processor usage: 67%" -- -- > > "load avg: %.2f %.2f %.2f" % 0.666 -- > "load avg: %0.67 ? ?" -- -- Please use '%' if you intend to further format values (chaining). (-%) :: Show a => String -> a -> String s -% x = s % x /% '?' infixl 9 -% -- | Replaces "%%" by "%". Any remaining occurrences of format strings are -- replaced by the given error character. Field width is respected when -- possible. -- -- > > "100%% %i" /% '?' -- > "100% ?" -- -- > > "100%% %03i" /% '?' -- > "100% ???" (/%) :: String -> Char -> String s /% errChar = depercent s where depercent ('%':s) = let (spec,s') = parseSpec s s'' = case ty spec of Percent -> "%" _ -> err errChar spec in s'' ++ depercent s' depercent (c:s) = c : depercent s depercent "" = "" -- | Aligns a string following a given spec. -- -- > align spec{width=1} "asdf" -- "asdf" -- > align spec{width=5} "asdf" -- " asdf" -- > align spec{width=5, leftAlign=True} "asdf" -- "asdf " align :: Spec -> String -> String align spec = U.align (S.leftAlign spec) (padWith spec) (width spec) duplicatePercents :: String -> String duplicatePercents ('%':s) = '%':'%':duplicatePercents s duplicatePercents (c:s) = c:duplicatePercents s duplicatePercents "" = "" -- | Formats two values into a string without finalizing: -- leaving duplicate percent signs & remaining format sequences. -- -- > > "%s %s!" %% ("Hello","World") -- > "Hello World!" -- -- > > "load avg: %.2f %.2f %.2f" %% (0.666,0.333) -- > "load avg: %0.67 %0.33 %.2f" -- -- In general: -- -- > s %% (x,y) == s % x % y -- -- Please use '-%%' if you don't intend to format values into a string any further. (%%) :: (Show a, Show b) => String -> (a,b) -> String s %% (x,y) = s % x % y -- | Formats three values into a string without finalizing. -- -- > > "load avg: %.2f %.2f %.2f" %%% (0.666,0.333,0.1) -- > "load avg: %0.67 %0.33 %0.10" (%%%) :: (Show a, Show b, Show c) => String -> (a,b,c) -> String s %%% (x,y,z) = s % x % y % z -- | Formats four values into a string without finalizing. (%%%%) :: (Show a, Show b, Show c, Show d) => String -> (a,b,c,d) -> String s %%%% (x,y,z,w) = s % x % y % z % w -- | Formats five values into a string without finalizing. (%%%%%) :: (Show a, Show b, Show c, Show d, Show e) => String -> (a,b,c,d,e) -> String s %%%%% (x,y,z,w,v) = s % x % y % z % w % v -- | Formats six values into a string without finalizing. (%%%%%%) :: (Show a, Show b, Show c, Show d, Show e, Show f) => String -> (a,b,c,d,e,f) -> String s %%%%%% (x,y,z,w,v,u) = s % x % y % z % w % v % u -- | Formats two values into a string and finalizes it: -- removing duplicate percent signs & replacing remaining format sequences -- with interrogation marks. -- -- > > "%s %s!" -%% ("Hello","World") -- > "Hello World!" -- -- > > "load avg: %.2f %.2f %.2f" -%% (0.666,0.333) -- > "load avg: %0.67 %0.33 ?" -- -- In general: -- -- > s -%% (x,y) == s % x -% y -- -- Please use '%%' if you intend to further format values. (-%%) :: (Show a, Show b) => String -> (a,b) -> String s -%% t = s %% t /% '?' -- | Formats three values into a string and finalizes it. -- -- > > "load avg: %.2f %.2f %.2f" -%%% (0.666,0.333,0.1) -- > "load avg: %0.67 %0.33 %0.10" (-%%%) :: (Show a, Show b, Show c) => String -> (a,b,c) -> String s -%%% t = s %%% t /% '?' -- | Formats four values into a string and finalizes it. (-%%%%) :: (Show a, Show b, Show c, Show d) => String -> (a,b,c,d) -> String s -%%%% t = s %%%% t /% '?' -- | Formats five values into a string and finalizes it. (-%%%%%) :: (Show a, Show b, Show c, Show d, Show e) => String -> (a,b,c,d,e) -> String s -%%%%% t = s %%%%% t /% '?' -- | Formats six values into a stirng and finalizes it. (-%%%%%%) :: (Show a, Show b, Show c, Show d, Show e, Show f) => String -> (a,b,c,d,e,f) -> String s -%%%%%% t = s %%%%%% t /% '?' -- | Just an alias to '%' for use whenever "Data.Ratio" is in scope. -- -- > import Data.Ratio -- > import Text.PercentFormat hiding ((%)) -- > "..." +% 1 -% 2 (+%) :: Show a => String -> a -> String (+%) = (%)