Skip to content

AJChapman/formatting

Repository files navigation

formatting Build Status Hackage

Formatting is a type-safe and flexible library for formatting text from built-in or custom data types.

Usage

You will probably need the OverloadedStrings language extension, and to import Formatting:

{-# LANGUAGE OverloadedStrings #-}

import Formatting

You may also need some or all of these:

import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TLB

Now a simple example:

> format ("Person's name is " % text % " and age is " % int) "Dave" 54
"Person's name is Dave and age is 54"

In this example, the formatters are two string literals (which take no arguments), and two formatters which take arguments: text, which takes a lazy Text, and int which takes any Integral, such as Int. They are all joined together using the % operator, producing a formatter which takes two arguments: a lazy Text and an Integral. It produces a lazy Text, because we used format. To produce other string types, or print the result instead, refer to this table:

To produce a use
TL.Text format
T.Text sformat
Builder bformat
String formatToString

To print the values instead, refer to this table:

To print to use
stdout fprint
stdout, appending a newline fprintLn
a handle hprint
a handle, appending a newline hprintLn

Apart from the % operator, formatters can also be joined using the monoid append operator (<>) to avoid repeating the same argument, they can be chained using %., and there are also formatter combinators for composing more advanced combinators. More on this below.

Formatter Quick Reference

Built-in formatters:

To format a e.g. as use short form
lazy Text "Hello" "Hello" text t
strict Text "World!" "World!" stext st
String "Goodbye" "Goodbye" string s
Builder "Bathtub" "Bathtub" builder
Show a => a [1, 2, 3] "[1, 2, 3]" shown sh
Char '!' "!" char c
Integral a => a 23 "23" int d
Real a => a 123.32 "123.32" float sf
Real a => a 123.32 "123.320" fixed 3 f
Scientific scientific 60221409 16 "6.0221409e23" sci
Scientific scientific 60221409 16 "6.022e23" scifmt Exponent (Just 3)
Buildable n, Integral n => n 123456 "12.34.56" groupInt 2 '.'
Buildable n, Integral n => n 12000 "12,000" commas
Integral n => n 32 "32nd" ords
Num a, Eq a => a 1 "1 ant" int <> plural "ant" "ants"
Num a, Eq a => a 2 "2 ants" int <> plural "ant" "ants"
Enum a => a a "97" asInt
Integral a => a 23 "10111" bin b
Integral a => a 23 "0b10111" prefixBin
Integral a => a 23 "27" oct o
Integral a => a 23 "0o27" prefixOct
Integral a => a 23 "17" hex x
Integral a => a 23 "0x17" prefixHex
Integral a => a 23 "13" base 20
Buildable a => a 10 " 10" left 4 ' ' l
Buildable a => a 10 "10 " right 4 ' ' r
Buildable a => a 10 " 10 " center 4 ' '
Buildable a => a 123456 "123" fitLeft 3
Buildable a => a 123456 "456" fitRight 3
Buildable a => a True "True" build
a undefined "gronk!" fconst "gronk!"

Formatter Combinator Quick Reference

Formatter combinators take a formatter and modify it somehow, e.g. by using it to format elements of a list, or changing its output.

Built-in formatter combinators:

To format a e.g. as use
Maybe a Nothing "Goodbye" maybed "Goodbye" text
Maybe a Just "Hello" "Hello" maybed "Goodbye" text
Maybe a Nothing "" optioned text
Maybe a Just "Hello" "Hello" optioned text
Either a b Left "Error!" "Error!" eithered text int
Either a b Right 69 "69" eithered text int
Either a x Left "bingo" "bingo" lefted text
Either a x Right 16 "" lefted text
Either x a Right "bingo" "bingo" righted text
Either x a Left 16 "" righted text
Foldable t => t a [1, 2, 3] "1st2nd3rd" concatenated ords
Foldable t => t a [123, 456, 789] "789456123" joinedWith (mconcat . reverse) int
Foldable t => t a [1, 2, 3] "1||2||3" intercalated "||" int
Foldable t => t a [1, 2, 3] "1 2 3" unworded int
Foldable t => t a [1, 2, 3] "1\n2\n3" unlined d
Foldable t => t a [1, 2, 3] "1 2 3" spaced int
Foldable t => t a [1, 2, 3] "1,2,3" commaSep int
Foldable t => t a [1, 2, 3] "1st, 2nd, 3rd" commaSpaceSep ords
Foldable t => t a ["one", "two", "three"] "[one, two, three]" list t
Foldable t => t a ["one", "two", "three"] "[\"one\", \"two\", \"three\"]" qlist t
[a] [1..] "[1, 10, 11, 100]" took 4 (list bin)
[a] [1..6] "[4, 5, 6]" dropped 3 (list int)
a "one two\tthree\nfour "one, two, three, four" splat isSpace commaSpaceSep stext
a 1234567890 "[123, 456, 789, 0]" splatWith (chunksOf 3) list int
a "one,two,three" "one\ntwo\nthree\n" splatOn "," unlined t
a "one two three " "[one, two, three]" worded list text
a "one\n\ntwo\nthree\n\n "["one", "", "two", "three", ""]" lined qlist text
a 123456 "654321" alteredWith TL.reverse int
a "Data.Char.isUpper "DCU" charsKeptIf isUpper string
a "Data.Char.isUpper "ata.har.ispper" charsRemovedIf isUpper string
a "look and boot" "leek and beet" replaced "oo" "ee" text
a "look and boot" "LOOK AND BOOT" uppercased
a "Look and Boot" "look and boot" lowercased
a "look and boot" "Look And Boot" titlecased
a "hellos" "he..." ltruncated 5 text
a "hellos" "h...s" ctruncated
a "hellos" "...os" rtruncated 5 text
a 1 " 1" lpadded 3 int
a 1 "1 " rpadded 3 int
a 1 " 1 " cpadded 3 int
a 123 "123 " lfixed 4 int
a 123456 "1..." lfixed 4 int
a 123 " 123" rfixed 4 int
a 123456 "...6" rfixed 4 int
a 123 " 123 " cfixed 2 1 ' ' int
a 1234567 "12...7" cfixed 2 1 ' ' int
a "Goo" "McGoo" prefixed "Mc" t
a "Goo" "Goosen" suffixed "sen" t
a "Goo" "McGooMc" surrounded "Mc" t
a "Goo" "McGoosen" enclosed "Mc" "sen" t
a "Goo" "'Goo'" squoted t
a "Goo" "\"Goo\"" dquoted t
a "Goo" "(Goo)" parenthesised t
a "Goo" "[Goo]" squared t
a "Goo" "{Goo}" braced t
a "Goo" "<Goo>" angled t
a "Goo" "`Goo`" backticked t
a "Goo" " Goo" indented 3 t
Foldable t => t a [1, 2, 3] " 1\n 2\n 3" indentedLines 2 d
a "1\n2\n3" " 1\n 2\n 3" reindented 2 t
Integral i, RealFrac d => d 6.66 "7" roundedTo int
Integral i, RealFrac d => d 6.66 "6" truncatedTo int
Integral i, RealFrac d => d 6.66 "7" ceilingedTo int
Integral i, RealFrac d => d 6.66 "6" flooredTo int
field through a Lens' s a (1, "goo") "goo" viewed _2 t
field through a record accessor s -> a (1, "goo") "1" accessed fst d
Integral a => a 4097 "0b0001000000000001" binPrefix 16
Integral a => a 4097 "0o0000000000010001" octPrefix 16
Integral a => a 4097 "0x0000000000001001" hexPrefix 16
Ord f, Integral a, Fractional f => a 1024 "1KB" bytes shortest
Ord f, Integral a, Fractional f => a 1234567890 "1.15GB" bytes (fixed 2)

Composing formatters

%. is like % but feeds one formatter into another:

λ> format (left 2 '0' %. hex) 10
"0a"

Using more than one formatter on the same argument

λ> now <- getCurrentTime
λ> format (year % "/" <> month <> "/" % dayOfMonth) now
"2015/01/27"

The Buildable Typeclass

One of the great things about formatting is that it doesn't rely on typeclasses: you can define one or more formatters for each of your types. But you also have the option of defining a 'default' formatter for a type, by implementing the Buildable typeclass, which has one method: build :: p -> Builder. Once this is defined for a type, you can use the build formatter (which is distinct from the build method of Buildable!):

> format ("Int: " % build % ", Text: " % build) 23 "hello"
"Int: 23, Text: hello"

Note that while this can be convenient, it also sacrifices some type-safety: there's nothing preventing you from putting the arguments in the wrong order, because both Int and Text have a Buildable instance. Note also that if a type already has a Show instance then you can use this instead, by using the shown formatter.

Understanding the Types

Formatters generally have a type like this:

Format r (a -> r)

This describes a formatter that will eventually produce some string type r, and takes an a as an argument. For example:

int :: Integral a => Format r (a -> r)

This takes an Integral a argument, and eventually produces an r. Let's work through using this with format:

-- format has this type:
format :: Format TL.Text a -> a

-- so in 'format int', called with an 'Int', 'int's type specialises to:
int :: Format TL.Text (Int -> TL.Text)

-- and 'format's 'a' parameter specialises to 'Int -> TL.Text':
format :: Format TL.Text (Int -> TL.Text) -> Int -> TL.Text

-- so 'format int' takes an Int and produces text:
format int :: Int -> TL.Text

What can be confusing in the above is that int's a parameter expands to Int, but format's a parameter expands to Int -> TL.Text.

Now let's look at what happens when we use the % operator to append formatters:

-- Here are the types of the functions we will use:
(%) :: Format r a -> Format r' r -> Format r' a
int :: Format r (Int -> r) -- simplified for this use
stext :: Format r (T.Text -> r)

-- Within the call to '%', in the expression 'int % stext', the type parameters expand like this:
-- r = T.Text -> r'
-- a = Int -> T.Text -> r'
-- and so we have these types:
int :: Format (T.Text -> r') (Int -> T.Text -> r')
stext :: Format r' (T.Text -> r')
int % stext :: Format r' (Int -> T.Text -> r')

-- And so when we use 'format' we get a function that takes two arguments and produces text:
format (int % stext) :: Int -> T.Text -> TL.Text

Comparison with Other Languages

Example:

format ("Person's name is " % text %  ", age is " % hex) "Dave" 54

or with short-names:

format ("Person's name is " % t % ", age is " % x) "Dave" 54

Similar to C's printf:

printf("Person's name is %s, age is %x","Dave",54);

and Common Lisp's FORMAT:

(format nil "Person's name is ~a, age is ~x" "Dave" 54)

Formatter Examples

"Hello, World!": Texts

> format (text % "!") "Hi!"
"Hi!!"
> format (string % "!") "Hi!"
"Hi!!"

123: Integers

> format int 23
"23"

23.4: Decimals

> format (fixed 0) 23.3
"23"
> format (fixed 2) 23.3333
"23.33"
> format shortest 23.3333
"23.3333"
> format shortest 0.0
"0.0"
> format sci 2.3
"2.3"
> format (scifmt Fixed (Just 0)) 2.3
"2"

1,242: Commas

> format commas 123456778
"123,456,778"
> format commas 1234
"1,234"

1st: Ordinals

> format ords 1
"1st"
> format ords 2
"2nd"
> format ords 3
"3rd"
> format ords 4
"4th"

3F: Hex

> format hex 15
"f"
> format hex 25
"19"

Monday 1st June: Dates & times

> now <- getCurrentTime
> later <- getCurrentTime
> format (dayOfMonth % "/" % month % "/" % year) now now now
"16/06/2014"
> format day now
"167"
> format hms now
"08:24:41"
> format tz now
"+0000"
> format datetime now
"Mon Jun 16 08:24:41 UTC 2014"
> format century now
"20"
> format (dayOfMonthOrd % " of " % monthName) now now
"16th of June"

3 years ago: Time spans

> format (diff False) (diffUTCTime later now)
"2 seconds"
> format (diff True) (diffUTCTime later now)
"in 2 seconds"
> format (diff True) (diffUTCTime now later)
"2 seconds ago"
> format (seconds 0 % " secs") (diffUTCTime now later)
"2 secs"
> let Just old = parseTime defaultTimeLocale "%Y" "1980" :: Maybe UTCTime
> format (years 0) (diffUTCTime now old)
"34"
> format (diff True) (diffUTCTime now old)
"in 35 years"
> format (diff True) (diffUTCTime old now)
"35 years ago"
> format (days 0) (diffUTCTime old now)
"12585"
> format (days 0 % " days") (diffUTCTime old now)
"12585 days"

File sizes

> format (bytes shortest) 1024
"1KB"
> format (bytes (fixed 2 % " ")) (1024*1024*5)
"5.00 MB"

Scientific

If you're using a type which provides its own builder, like the Scientific type:

import Data.Text.Lazy.Builder.Scientific
scientificBuilder :: Scientific -> Builder
formatScientificBuilder :: FPFormat -> Maybe Int -> Scientific -> Builder

Then you can use later easily:

> format (later scientificBuilder) 23.4
"23.4"

Actually, there are now already two handy combinators (sci and scifmt) for the Scientific type as shown above in the Decimals section.

Writing your own Formatters

You can include things verbatim in the formatter:

> format (now "This is printed now.")
"This is printed now."

Although with OverloadedStrings you can just use string literals:

> format "This is printed now."
"This is printed now."

You can handle things later which makes the formatter accept arguments:

> format (later (const "This is printed later.")) ()
"This is printed later."

The type of the function passed to later should return an instance of Monoid.

later :: (a -> Builder) -> Format r (a -> r)

The function you format with (format, bprint, etc.) will determine the monoid of choice. In the case of this library, the top-level formating functions expect you to build a text Builder:

format :: Format Text a -> a

Because builders are efficient generators.

So in this case we will be expected to produce Builders from arguments:

format . later :: (a -> Builder) -> a -> Text

To do that for common types you can just re-use the formatting library and use bprint:

λ> :t bprint
bprint :: Format Builder a -> a
> :t bprint int 23
bprint int 23 :: Builder

Coming back to later, we can now use it to build our own printer combinators:

> let mint = later (maybe "" (bprint int))
> :t mint
mint :: Integral a => Format r (Maybe a -> r)

Now mint is a formatter to show Maybe Integer:

> format mint (readMaybe "23")
"23"
> format mint (readMaybe "foo")
""

Although a better, more general combinator might be:

> let mfmt x f = later (maybe x (bprint f))

Now you can use it to maybe format things:

> format (mfmt "Nope!" int) (readMaybe "foo")
"Nope!"

Using it with other APIs

As a convenience, we provide the FromBuilder typeclass and the formatted combinator. formatted makes it simple to add formatting to any API that is expecting a Builder, a strict or lazy Text, or a String. For example if you have functions logDebug, logWarning and logInfo all of type Text -> IO () you can do the following:

> formatted logDebug ("x is: " % int) x
> formatted logInfo ("y is: " % squared int) y
> formatted logWarning ("z is: " % braced int) z

The above example will work for either strict or lazy Text

Hacking

Building with Nix

See README-nix.md.

Running the Tests

From within your nix-shell, run cabal test.

The tests are in test/Spec.hs.

Running the Benchmarks

Start nix-shell like this: nix-shell --arg doBenchmark true. From within your nix-shell, run cabal bench.

To build the html benchmarking reports, run cabal bench --benchmark-option=-obench/reports/7.2.0.html > bench/reports/7.2.0.txt, replacing '7.2.0' with the current version. This will output the file bench/reports/7.2.0.html which you can open in a browser, and bench/reports/7.2.0.txt which you can view in a terminal or text editor.

The benchmarks are in bench/bench.hs.