diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Tony Day (c) 2017
+
+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 Tony Day 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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/formatn.cabal b/formatn.cabal
new file mode 100644
--- /dev/null
+++ b/formatn.cabal
@@ -0,0 +1,64 @@
+cabal-version:  2.4
+name:           formatn
+version:        0.0.1
+synopsis:       Number text formatting.
+description:    This package provides support for common number formatting patterns.
+    .
+    == Usage
+    .
+    > import Data.FormatN
+    .
+    See doctests for functionality and range.
+
+category:       Development
+homepage:       https://github.com/tonyday567/formatn#readme
+bug-reports:    https://github.com/tonyday567/formatn/issues
+author:         Tony Day
+maintainer:     tonyday567@gmail.com
+copyright:      2016 Tony Day
+license:        BSD-3-Clause
+license-file:   LICENSE
+build-type:     Simple
+
+source-repository head
+  type: git
+  location: https://github.com/tonyday567/formatn
+
+library
+  hs-source-dirs:
+    src
+  default-extensions:
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+  build-depends:
+    attoparsec >= 0.13 && < 0.14,
+    base >=4.7 && <5,
+    containers >= 0.6,
+    foldl >= 1.4,
+    generic-lens >= 1.2 && < 3.0,
+    numhask >= 0.7 && < 0.8,
+    scientific >= 0.3,
+    tdigest >= 0.2,
+    text >= 1.2,
+    transformers >= 0.5
+  exposed-modules:
+    Data.FormatN
+  other-modules:
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: test.hs
+  hs-source-dirs:
+      test
+  default-extensions:
+  build-depends:
+    base >=4.7 && <5,
+    doctest >= 0.16 && < 0.18,
+    formatn,
+    numhask >= 0.7 && < 0.8,
+  default-language: Haskell2010
diff --git a/src/Data/FormatN.hs b/src/Data/FormatN.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FormatN.hs
@@ -0,0 +1,218 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RebindableSyntax #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# OPTIONS_GHC -Wall #-}
+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
+{-# OPTIONS_GHC -fno-warn-type-defaults #-}
+
+-- | Formatting of 'Double's.
+module Data.FormatN
+  ( FormatN (..),
+    defaultFormatN,
+    fromFormatN,
+    toFormatN,
+    fixed,
+    decimal,
+    prec,
+    comma,
+    expt,
+    dollar,
+    formatN,
+    precision,
+    formatNs,
+  )
+where
+
+import Data.Generics.Labels ()
+import Data.List (nub)
+import Data.Scientific
+import qualified Data.Text as Text
+import NumHask.Prelude
+
+-- | Wrapper for the various formatting options.
+--
+-- >>> defaultFormatN
+-- FormatComma 2
+data FormatN
+  = FormatFixed Int
+  | FormatDecimal Int
+  | FormatComma Int
+  | FormatExpt Int
+  | FormatPrec Int
+  | FormatDollar Int
+  | FormatPercent Int
+  | FormatNone
+  deriving (Eq, Show, Generic)
+
+-- | The official format
+defaultFormatN :: FormatN
+defaultFormatN = FormatComma 2
+
+-- | make text
+formatN :: FormatN -> Double -> Text
+formatN (FormatFixed n) x = fixed n x
+formatN (FormatDecimal n) x = decimal n x
+formatN (FormatPrec n) x = prec n x
+formatN (FormatComma n) x = comma n x
+formatN (FormatExpt n) x = expt n x
+formatN (FormatDollar n) x = dollar n x
+formatN (FormatPercent n) x = percent n x
+formatN FormatNone x = pack (show x)
+
+-- | type textifier
+fromFormatN :: (IsString s) => FormatN -> s
+fromFormatN (FormatFixed _) = "Fixed"
+fromFormatN (FormatDecimal _) = "Decimal"
+fromFormatN (FormatComma _) = "Comma"
+fromFormatN (FormatExpt _) = "Expt"
+fromFormatN (FormatPrec _) = "Prec"
+fromFormatN (FormatDollar _) = "Dollar"
+fromFormatN (FormatPercent _) = "Percent"
+fromFormatN FormatNone = "None"
+
+-- | type readifier
+toFormatN :: (Eq s, IsString s) => s -> Int -> FormatN
+toFormatN "Fixed" n = FormatFixed n
+toFormatN "Decimal" n = FormatDecimal n
+toFormatN "Comma" n = FormatComma n
+toFormatN "Expt" n = FormatExpt n
+toFormatN "Prec" n = FormatPrec n
+toFormatN "Dollar" n = FormatDollar n
+toFormatN "Percent" n = FormatPercent n
+toFormatN "None" _ = FormatNone
+toFormatN _ _ = FormatNone
+
+-- | to x decimal places
+--
+-- >>> fixed 2 1
+-- "1.00"
+--
+-- >>> fixed 2 0.001
+-- "0.00"
+fixed :: Int -> Double -> Text
+fixed x n = pack $ formatScientific Fixed (Just x) (fromFloatDigits n)
+
+-- | scientific exponential
+--
+-- >>> expt 2 1234
+-- "1.23e3"
+expt :: Int -> Double -> Text
+expt x n = pack $ formatScientific Exponent (Just x) (fromFloatDigits n)
+
+-- | round to n significant figures
+--
+-- >>> roundSig 2 1234
+-- 1230.0
+--
+-- >>> roundSig 2 0.001234
+-- 1.23e-3
+roundSig :: Int -> Double -> Scientific
+roundSig n x = scientific r' (e - length ds0)
+  where
+    (ds, e) = toDecimalDigits $ fromFloatDigits x
+    (ds0, ds1) = splitAt (n + 1) ds
+    r =
+      (fromIntegral $ foldl' (\x a -> x * 10 + a) 0 ds0 :: Double)
+        + fromIntegral (foldl' (\x a -> x * 10 + a) 0 ds1) / (10.0 ^ (length ds1 :: Int))
+    r' = round r :: Integer
+
+-- | format numbers between 0.001 and 1,000,000 using digit and comma notation and exponential outside this range, with x significant figures.
+--
+-- >>> prec 1 0.00234
+-- "0.0023"
+--
+-- >>> prec 1 0.000023
+-- "2.3e-5"
+--
+-- >>> prec 1 123
+-- "120"
+--
+-- >>> prec 1 123456
+-- "120000"
+--
+-- >>> prec 1 1234567
+-- "1.2e6"
+prec :: Int -> Double -> Text
+prec n x
+  | x < 0 = "-" <> prec n (- x)
+  | x == 0 = "0"
+  | x < 0.001 = expt n x
+  | x > 1e6 = expt n x
+  | otherwise = decimal n (toRealFloat x')
+  where
+    x' = maybe fromFloatDigits roundSig (Just n) x
+
+-- | round to n significant figures and always use decimal notation
+--
+-- >>> decimal 2 0.000001234
+-- "0.00000123"
+--
+-- >>> decimal 2 1234567
+-- "1230000"
+decimal :: Int -> Double -> Text
+decimal n x = x''
+  where
+    x' = pack $ formatScientific Fixed Nothing $ maybe fromFloatDigits roundSig (Just n) x
+    x'' = (\x -> bool x' (fst x) (snd x == ".0")) $ Text.breakOn "." x'
+
+-- | add commas format for numbers above 1,000 but below 1 million, otherwise use prec.
+--
+-- >>> comma 2 1234
+-- "1,230"
+comma :: Int -> Double -> Text
+comma n x
+  | x < 0 = "-" <> comma n (- x)
+  | x < 1000 || x > 1e6 = prec n x
+  | otherwise = addcomma (prec n x)
+  where
+    addcomma x = uncurry (<>) . first (Text.reverse . Text.intercalate "," . Text.chunksOf 3 . Text.reverse) $ Text.breakOn "." x
+
+-- | dollars and cents, always decimal notation
+--
+-- >>> dollar 2 1234
+-- "$1,230"
+--
+-- >>> dollar 2 0.01234
+-- "$0.0123"
+dollar :: Int -> Double -> Text
+dollar n x
+  | x < 0 = "-" <> dollar n (- x)
+  | otherwise = "$" <> comma n x
+
+-- | fixed percent, always decimal notation
+--
+-- >>> percent 2 0.001234
+-- "0.123%"
+percent :: Int -> Double -> Text
+percent n x = (<> "%") $ decimal n (100 * x)
+
+-- | Provide formatted text for a list of numbers so that they are just distinguished.  'precision commas 2 ticks' means use as much precision as is needed for them to be distinguished, but with at least 2 significant figures.
+precision :: (Int -> Double -> Text) -> Int -> [Double] -> [Text]
+precision f n0 xs =
+  precLoop f n0 xs
+  where
+    precLoop f' n xs' =
+      let s = f' n <$> xs'
+       in if s == nub s || n > 4
+            then s
+            else precLoop f' (n + 1) xs'
+
+-- | Consistently format a list of doubles.
+formatNs :: FormatN -> [Double] -> [Text]
+formatNs (FormatFixed n) xs = precision fixed n xs
+formatNs (FormatDecimal n) xs = precision decimal n xs
+formatNs (FormatPrec n) xs = precision prec n xs
+formatNs (FormatComma n) xs = precision comma n xs
+formatNs (FormatExpt n) xs = precision expt n xs
+formatNs (FormatDollar n) xs = precision dollar n xs
+formatNs (FormatPercent n) xs = precision percent n xs
+formatNs FormatNone xs = pack . show <$> xs
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE RebindableSyntax #-}
+{-# OPTIONS_GHC -Wall #-}
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+
+module Main where
+
+import NumHask.Prelude
+import Test.DocTest
+
+main :: IO ()
+main =
+  doctest
+  [ "src/Data/FormatN.hs"
+  ]
