formatn 0.1.0 → 0.2.0
raw patch · 2 files changed
+405/−200 lines, 2 filesdep +QuickCheckPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck
API changes (from Hackage documentation)
- Data.FormatN: FormatComma :: Maybe Int -> FormatN
- Data.FormatN: FormatDecimal :: Maybe Int -> FormatN
- Data.FormatN: FormatDollar :: Maybe Int -> FormatN
- Data.FormatN: FormatExpt :: Maybe Int -> FormatN
- Data.FormatN: FormatFixed :: Maybe Int -> FormatN
- Data.FormatN: FormatNone :: FormatN
- Data.FormatN: FormatPercent :: Maybe Int -> FormatN
- Data.FormatN: FormatPrec :: Maybe Int -> FormatN
- Data.FormatN: [exponent] :: SigFig -> Int
- Data.FormatN: [figures] :: SigFig -> Integer
- Data.FormatN: [sign] :: SigFig -> SigFigSign
- Data.FormatN: precision :: (Maybe Int -> Double -> Text) -> Maybe Int -> [Double] -> [Text]
- Data.FormatN: showOr :: FormatN -> Double -> Text
+ Data.FormatN: CommaStyle :: FormatStyle
+ Data.FormatN: DecimalStyle :: FormatStyle
+ Data.FormatN: DollarStyle :: FormatStyle
+ Data.FormatN: ExponentStyle :: Maybe Int -> FormatStyle
+ Data.FormatN: FSComma :: FStyle
+ Data.FormatN: FSCommaPrec :: FStyle
+ Data.FormatN: FSDecimal :: FStyle
+ Data.FormatN: FSDollar :: FStyle
+ Data.FormatN: FSExponent :: Maybe Int -> FStyle
+ Data.FormatN: FSFixed :: Int -> FStyle
+ Data.FormatN: FSNone :: FStyle
+ Data.FormatN: FSPercent :: FStyle
+ Data.FormatN: FSPrec :: FStyle
+ Data.FormatN: FixedStyle :: Int -> FormatStyle
+ Data.FormatN: FormatN :: FStyle -> Maybe Int -> Bool -> FormatN
+ Data.FormatN: PercentStyle :: FormatStyle
+ Data.FormatN: [addLPad] :: FormatN -> Bool
+ Data.FormatN: [fstyle] :: FormatN -> FStyle
+ Data.FormatN: [sfExponent] :: SigFig -> Int
+ Data.FormatN: [sfFigures] :: SigFig -> Integer
+ Data.FormatN: [sfSign] :: SigFig -> SigFigSign
+ Data.FormatN: [sigFigs] :: FormatN -> Maybe Int
+ Data.FormatN: commaPrec :: Maybe Int -> Double -> Text
+ Data.FormatN: commaPrecStyle :: Double -> FormatStyle
+ Data.FormatN: commaSF :: SigFig -> Text
+ Data.FormatN: data FStyle
+ Data.FormatN: data FormatStyle
+ Data.FormatN: decimalSF :: SigFig -> Text
+ Data.FormatN: distinguish :: Int -> Bool -> (Double -> FormatStyle) -> Maybe Int -> [Double] -> [Text]
+ Data.FormatN: dollarSF :: (SigFig -> Text) -> SigFig -> Text
+ Data.FormatN: exptSF :: SigFig -> Text
+ Data.FormatN: exptSFWith :: Maybe Int -> SigFig -> Text
+ Data.FormatN: exptWith :: Maybe Int -> Maybe Int -> Double -> Text
+ Data.FormatN: fixedSF :: Maybe Int -> SigFig -> Text
+ Data.FormatN: format :: FormatStyle -> Maybe Int -> Double -> Text
+ Data.FormatN: formatOrShow :: FormatStyle -> Maybe Int -> Double -> Text
+ Data.FormatN: formatSF :: FormatStyle -> SigFig -> Text
+ Data.FormatN: formats :: Bool -> (Double -> FormatStyle) -> Maybe Int -> [Double] -> [Text]
+ Data.FormatN: incSigFig :: Int -> SigFig -> SigFig
+ Data.FormatN: instance GHC.Classes.Eq Data.FormatN.FStyle
+ Data.FormatN: instance GHC.Classes.Eq Data.FormatN.FormatStyle
+ Data.FormatN: instance GHC.Classes.Ord Data.FormatN.FStyle
+ Data.FormatN: instance GHC.Classes.Ord Data.FormatN.FormatStyle
+ Data.FormatN: instance GHC.Show.Show Data.FormatN.FStyle
+ Data.FormatN: instance GHC.Show.Show Data.FormatN.FormatStyle
+ Data.FormatN: majorityStyle :: (Double -> FormatStyle) -> [Double] -> FormatStyle
+ Data.FormatN: percentSF :: (SigFig -> Text) -> SigFig -> Text
+ Data.FormatN: precStyle :: Double -> FormatStyle
- Data.FormatN: dollar :: Maybe Int -> Double -> Text
+ Data.FormatN: dollar :: (SigFig -> Text) -> Maybe Int -> Double -> Text
- Data.FormatN: formatNs :: FormatN -> [Double] -> [Text]
+ Data.FormatN: formatNs :: Int -> FormatN -> [Double] -> [Text]
- Data.FormatN: percent :: Maybe Int -> Double -> Text
+ Data.FormatN: percent :: (SigFig -> Text) -> Maybe Int -> Double -> Text
Files
- formatn.cabal +3/−4
- src/Data/FormatN.hs +402/−196
formatn.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: formatn-version: 0.1.0+version: 0.2.0 synopsis: Formatting of doubles. description: This package provides support for number formatting styles, especially styles involving significant figure calculations.@@ -37,9 +37,8 @@ , base >=4.7 && <5 , containers ^>=0.6 , text ^>=1.2-- -- uncomment to run cabal-docspec --check-properties- -- QuickCheck,+ -- just for cabal-docspec --check-properties+ , QuickCheck exposed-modules: Data.FormatN other-modules: default-language: Haskell2010
src/Data/FormatN.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StrictData #-}+{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -Wall #-} -- | Text formatting of 'Double's.@@ -22,32 +23,58 @@ -- <https://hackage.haskell.org/package/rounded rounded> seems to be much more about doing computation taking rounding into account, compared with the much simpler task of pretty printing a number. -- -- This library could have just provided an ability to compute a significant figure version of a number and then use these other libraries, but the round trip (from Double to SigFig to Double) introduces errors (eg the least significant figure goes from being a '4' to a '3999999' via float maths).+--+-- formatn is used in the <https://hackage.haskell.org/package/chart-svg chart-svg> library to automate consistent number formatting across different scales. module Data.FormatN ( -- * Usage- -- $usage-- -- * FormatN- FormatN (..),- defaultFormatN,- formatN,- formatNs,- precision,+ -- $setup -- * SigFig SigFig (..), SigFigSign (..), toSigFig, fromSigFig,+ incSigFig, - -- * formatters+ -- * specific formatters+ FormatStyle (..),+ precStyle,+ commaPrecStyle,+ FStyle (..),++ -- * SigFig formatters+ fixedSF,+ exptSF,+ exptSFWith,+ decimalSF,+ commaSF,+ dollarSF,+ percentSF,+ formatSF,++ -- * specific Double formatters+ format,+ formatOrShow, fixed, expt,+ exptWith, decimal, prec, comma,+ commaPrec, dollar, percent,- showOr,++ -- * list modifiers+ majorityStyle,+ formats,+ distinguish,++ -- * FormatN+ FormatN (..),+ defaultFormatN,+ formatN,+ formatNs, ) where @@ -55,12 +82,15 @@ import Data.Bool import Data.Containers.ListUtils (nubOrd) import Data.Foldable+import qualified Data.List as List+import qualified Data.Map.Strict as Map import Data.Maybe+import Data.Ord import Data.Text (Text, pack) import qualified Data.Text as Text import GHC.Generics hiding (prec) import Numeric-import Prelude+import Prelude hiding (exponent) -- $setup -- >>> import Data.FormatN@@ -72,101 +102,19 @@ -- >>> decimal (Just 2) <$> xs -- ["-1.0","0.0","1.0","1.0","1.0","1.1","1.2"] ----- >>> comma (Just 3) <$> xs--- ["-1.00","0.00","1.00","1.01","1.02","1.10","1.20"]------ comma (Just 3) . (1e3*) <$> xs--- ["-1,000","0.00","1,000","1,010","1,020","1,100","1,200"]------ comma (Just 3) . (1e-3*) <$> xs+-- >>> decimal (Just 3) . (1e-3*) <$> xs -- ["-0.00100","0.00","0.00100","0.00101","0.00102","0.00110","0.00120"] ----- >>> comma (Just 3) . (1e-6*) <$> xs--- ["-1.00e-6","0.00","1.00e-6","1.01e-6","1.02e-6","1.10e-6","1.20e-6"]------ >>> comma (Just 2) . (1e3*) <$> xs--- ["-1,000","0.0","1,000","1,000","1,000","1,100","1,200"]------ >>> precision comma (Just 2) $ (1e3*) <$> [0,1,1.01,1.02,1.1,1.2]--- ["0.00","1,000","1,010","1,020","1,100","1,200"]---- $usage--- >>> import Data.FormatN--- >>> xs = [(-1),0,1,1.01,1.02,1.1,1.2]--- >>> fixed (Just 2) <$> xs--- ["-1.00","0.00","1.00","1.01","1.02","1.10","1.20"]------ >>> decimal (Just 2) <$> xs--- ["-1.0","0.0","1.0","1.0","1.0","1.1","1.2"]------ formatn is used in the <https://hackage.haskell.org/package/chart-svg chart-svg> library to automate consistent number formatting across different scales.------ >>> comma (Just 3) <$> xs--- ["-1.00","0.00","1.00","1.01","1.02","1.10","1.20"]--- -- >>> comma (Just 3) . (1e3*) <$> xs -- ["-1,000","0.00","1,000","1,010","1,020","1,100","1,200"] ----- >>> comma (Just 3) . (1e-3*) <$> xs--- ["-0.00100","0.00","0.00100","0.00101","0.00102","0.00110","0.00120"]------ >>> comma (Just 3) . (1e-6*) <$> xs--- ["-1.00e-6","0.00","1.00e-6","1.01e-6","1.02e-6","1.10e-6","1.20e-6"]------ Using significant figures actually changes numbers - numbers that were slightly different end up being (and looking like) the same. 'precision' increases the number of significant figures to get around this.------ >>> comma (Just 2) . (1e3*) <$> xs--- ["-1,000","0.0","1,000","1,000","1,000","1,100","1,200"]------ >>> precision comma (Just 2) $ (1e3*) <$> [0,1,1.01,1.02,1.1,1.2]--- ["0.00","1,000","1,010","1,020","1,100","1,200"]------ Also note the clunkiness of the treatment of zero. It is problematic to default format zero consistently.---- | Wrapper for the various formatting options.------ Nothing in the context of these constructors means do not perform and significant figure adjustments to the numbers (or decimal figures with respect to FormatFixed).-data FormatN- = FormatFixed (Maybe Int)- | FormatDecimal (Maybe Int)- | FormatComma (Maybe Int)- | FormatExpt (Maybe Int)- | FormatPrec (Maybe Int)- | FormatDollar (Maybe Int)- | FormatPercent (Maybe Int)- | FormatNone- deriving (Eq, Show, Generic)---- | The official format------ >>> defaultFormatN--- FormatComma (Just 2)-defaultFormatN :: FormatN-defaultFormatN = FormatComma (Just 2)---- | run a 'FormatN'------ >>> formatN defaultFormatN 1234--- "1,200"-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)---- | Format to x decimal places with no significant figure rounding.+-- Using significant figures actually changes numbers - numbers that were slightly different end up being (and looking like) the same. 'distinguish' increases the number of significant figures to get around this. ----- >>> fixed (Just 2) 100--- "100.00"+-- >>> formats False (const CommaStyle) (Just 2) $ (1e3*) <$> xs+-- ["-1,000","0","1,000","1,000","1,000","1,100","1,200"] ----- >>> fixed (Just 2) 0.001--- "0.00"-fixed :: Maybe Int -> Double -> Text-fixed n x = pack $ showFFloat n x ""+-- >>> distinguish 4 False (const CommaStyle) (Just 2) $ (1e3*) <$> xs+-- ["-1,000","0","1,000","1,010","1,020","1,100","1,200"] -- | Decomposition of a Double into the components that are needed to determine significant figure formatting. --@@ -177,11 +125,11 @@ -- \] data SigFig = SigFig { -- | sign- sign :: SigFigSign,+ sfSign :: SigFigSign, -- | significant figures expressed as an Integer- figures :: Integer,+ sfFigures :: Integer, -- | the power of 10 exponent given figures.- exponent :: Int+ sfExponent :: Int } deriving (Eq, Show) @@ -194,11 +142,11 @@ -- | convert from a Double to a 'SigFig' -- -- >>> toSigFig (Just 2) 1234--- SigFig {sign = SigFigPos, figures = 12, exponent = 2}+-- SigFig {sfSign = SigFigPos, sfFigures = 12, sfExponent = 2} -- -- prop> \x -> let (SigFig s fs e) = toSigFig Nothing x in let x' = ((if (s==SigFigNeg) then (-1.0) else 1.0) * fromIntegral fs * 10.0**fromIntegral e) in (x==0 || abs (x/x'-1) < 1e-6) toSigFig :: Maybe Int -> Double -> SigFig-toSigFig n x = SigFig s fs' expo+toSigFig n x = SigFig s fs' expo' where (s, (floatfs, floate)) = bool (SigFigPos, floatToDigits 10 x) (SigFigNeg, floatToDigits 10 (-x)) (x < 0) -- floatToDigits 10 0 == ([0],0) floatToDigits 10 1 == ([1],1)@@ -207,9 +155,9 @@ -- pad with extra zeros if less figures than requested (floatfs', e) = bool- (floatfs, floate' - length floatfs)- (floatfs <> replicate (nsig - length floatfs) 0, floate' - nsig)- (length floatfs < nsig)+ (floatfs, floate' - length floatfs)+ (floatfs <> replicate (nsig - length floatfs) 0, floate' - nsig)+ (length floatfs < nsig) (fs0, fs1) = splitAt nsig floatfs' -- reconstitute number to get rounding right at the least significance point fs =@@ -219,21 +167,34 @@ -- rounding can bump significant figures by 1 eg 99(.9999) ==> 100 (fs', expo) = bool- (fs, e + length floatfs' - nsig)- (fs `div` 10, e + length floatfs' - nsig + 1)- (length (show fs) > nsig)+ (fs, e + length floatfs' - nsig)+ (fs `div` 10, e + length floatfs' - nsig + 1)+ (length (show fs) > nsig)+ -- zero fix+ expo' = bool expo 0 (fs' == 0 && expo > 0) -- | convert from a 'SigFig' to a Double -- -- >>> fromSigFig (SigFig SigFigPos 12 2) -- 1200.0------ @fromSigFig . toSigFig Nothing@ may not be isomorphic fromSigFig :: SigFig -> Double fromSigFig (SigFig s fs e) = bool 1 (-1) (s == SigFigNeg) * fromIntegral fs * 10 ** fromIntegral e -exptR :: SigFig -> Text-exptR (SigFig s i e) = pack $ sfsign s <> i'' <> "e" <> show e'+-- | increase significant figures+incSigFig :: Int -> SigFig -> SigFig+incSigFig n (SigFig s fs e) = SigFig s (fs * (10 ^ max 0 n)) (e - n)++-- The natural exponent to format with+eSF :: SigFig -> Int+eSF (SigFig _ fs e) = e + length (show fs) - 1++-- | expt format for a SigFig+fixedSF :: Maybe Int -> SigFig -> Text+fixedSF n sf = fixed n (fromSigFig sf)++-- | expt format for a SigFig+exptSF :: SigFig -> Text+exptSF (SigFig s i e) = pack $ sfsign s <> i'' <> "e" <> show e' where i'' | length i' == 1 = i'@@ -241,31 +202,97 @@ i' = show i e' = e + length i' - 1 --- | Format in exponential style, maybe with significant figure rounding.+-- | expt format for a SigFig, with an exponent override ----- >>> expt Nothing 1245--- "1.245e3"+-- >>> exptSFWith (Just 1) (toSigFig (Just 1) 1)+-- "0.1e1" ----- >>> expt (Just 3) 1245--- "1.24e3"+-- >>> exptSFWith (Just 0) (toSigFig (Just 1) 1)+-- "1e0" ----- >>> expt (Just 3) 0.1245--- "1.24e-1"-expt :: Maybe Int -> Double -> Text-expt n x = exptR (toSigFig n x)+-- >>> exptSFWith (Just (-1)) (toSigFig (Just 1) 1)+-- "10e-1"+exptSFWith :: Maybe Int -> SigFig -> Text+exptSFWith eover (SigFig s i e) = pack (sfsign s) <> posDecimalSF i (e - e') <> "e" <> pack (show e')+ where+ e' = fromMaybe (e + length (show i) - 1) eover -decimalR :: SigFig -> Text-decimalR (SigFig s xs e) = pack $ sfsign s <> t+-- Formatting the positive component in decimal style+posDecimalSF :: Integer -> Int -> Text+posDecimalSF xs e = pack t where xs' = show xs nsf = length xs' extrasf = bool (-(e + nsf)) (-(e + nsf)) (xs == 0) oversf = length xs' + e t- | e >= 0 = xs' <> replicate e '0'+ | e >= 0 = bool (xs' <> replicate e '0') xs' (xs == 0) | e <= -nsf = "0." <> replicate extrasf '0' <> xs' | otherwise = take oversf xs' <> "." <> drop oversf xs' +-- | comma format for a SigFig+maybeCommaSF :: Bool -> SigFig -> Text+maybeCommaSF doCommas (SigFig s xs e) = pack (sfsign s) <> bool id addcommas doCommas (posDecimalSF xs e)+ where+ addcommas =+ uncurry (<>)+ . first (Text.reverse . Text.intercalate "," . Text.chunksOf 3 . Text.reverse)+ . Text.breakOn "."++-- | comma format for a SigFig+commaSF :: SigFig -> Text+commaSF = maybeCommaSF True++-- | decimal format for a SigFig+decimalSF :: SigFig -> Text+decimalSF = maybeCommaSF False++-- | percent format for a SigFig+percentSF :: (SigFig -> Text) -> SigFig -> Text+percentSF f (SigFig s figs e) = (<> "%") $ f (SigFig s figs (e + 2))++-- | dollar format for a SigFig+dollarSF :: (SigFig -> Text) -> SigFig -> Text+dollarSF f sf =+ case sfSign sf of+ SigFigNeg -> "-" <> dollarSF f (SigFig SigFigPos (sfFigures sf) (sfExponent sf))+ SigFigPos -> "$" <> f sf++-- * specific number formats++-- | Format to x decimal places with no significant figure rounding.+--+-- >>> fixed (Just 2) 100+-- "100.00"+--+-- >>> fixed (Just 2) 0.001+-- "0.00"+fixed :: Maybe Int -> Double -> Text+fixed n x = pack $ showFFloat n x ""++-- | Format in exponential style, maybe with significant figure rounding.+--+-- >>> expt Nothing 1245+-- "1.245e3"+--+-- >>> expt (Just 3) 1245+-- "1.24e3"+--+-- >>> expt (Just 3) 0.1245+-- "1.24e-1"+expt :: Maybe Int -> Double -> Text+expt n x = exptSF (toSigFig n x)++-- | Format in exponential style, with the suggested exponent.+--+-- >>> exptWith (Just 2) Nothing 1245+-- "12.45e2"+--+-- >>> exptWith (Just 6) (Just 3) 1245+-- "0.00124e6"+exptWith :: Maybe Int -> Maybe Int -> Double -> Text+exptWith n' n x = exptSFWith n' (toSigFig n x)+ -- | Format in decimal style, and maybe round to n significant figures. -- -- >>> decimal Nothing 1.2345e-2@@ -277,10 +304,139 @@ -- >>> decimal (Just 2) 12345 -- "12000" decimal :: Maybe Int -> Double -> Text-decimal n x = decimalR (toSigFig n x)+decimal n x = decimalSF (toSigFig n x) --- | Format between 0.001 and 1,000,000 using decimal style and exponential style outside this range.+-- | Format with US-style commas --+-- >>> comma (Just 3) 1234567+-- "1,230,000"+comma :: Maybe Int -> Double -> Text+comma n x = commaSF (toSigFig n x)++-- | Adjust format to a percent.+--+-- >>> percent commaSF (Just 3) 0.1234+-- "12.3%"+--+-- >>> percent decimalSF (Just 1) 0.1234+-- "10%"+percent :: (SigFig -> Text) -> Maybe Int -> Double -> Text+percent f n x = percentSF f (toSigFig n x)++-- | Adjust format to dollar style.+--+-- >>> dollar commaSF (Just 3) 1234+-- "$1,230"+--+-- >>> dollar (fixedSF (Just 2)) (Just 2) 0.01234+-- "$0.01"+dollar :: (SigFig -> Text) -> Maybe Int -> Double -> Text+dollar f n x = dollarSF f (toSigFig n x)++-- | Data type representing styles of formatting+data FormatStyle+ = -- | 1000 1 0.001+ DecimalStyle+ | -- | The parameter represents the exponent to format to with Nothing meaning:+ --+ -- 1e3 1e1 1e-3+ ExponentStyle (Maybe Int)+ | -- | 1,000 1 0.001+ CommaStyle+ | -- | 1000.00 1.00 0.00+ FixedStyle Int+ | -- | 100,000% 100% 0.1%+ PercentStyle+ | -- | \$1,000 $1 $0.001+ DollarStyle+ deriving (Show, Eq, Ord)++-- | DecimalStyle between 0.001 and 1000000 and ExponentStyle outside this range.+precStyle :: Double -> FormatStyle+precStyle x+ | x == 0 = DecimalStyle+ | abs x < 0.001 = ExponentStyle (Just (eSF (toSigFig Nothing x)))+ | abs x > 1e6 = ExponentStyle (Just (eSF (toSigFig Nothing x)))+ | otherwise = DecimalStyle++-- | CommaStyle above 1,000 but below a million, DecimalStyle between 0.001 and a million and ExponentStyle outside this range.+commaPrecStyle :: Double -> FormatStyle+commaPrecStyle x+ | x == 0 = CommaStyle+ | abs x < 0.001 = ExponentStyle (Just (eSF (toSigFig Nothing x)))+ | abs x > 1e6 = ExponentStyle (Just (eSF (toSigFig Nothing x)))+ | otherwise = CommaStyle++-- | Data type representing styles of formatting dependent on the number+data FStyle+ = FSDecimal+ | FSExponent (Maybe Int)+ | FSComma+ | FSFixed Int+ | FSPercent+ | FSDollar+ | FSPrec+ | FSCommaPrec+ | FSNone+ deriving (Show, Eq, Ord)++-- | Compute the majority FormatStyle so a list of numbers can all have the same formatting+--+-- Also equalises the exponent to majority for exponent style.+--+-- >>> commaPrecStyle <$> [0,5e6,1e7,2e7]+-- [CommaStyle,ExponentStyle (Just 6),ExponentStyle (Just 7),ExponentStyle (Just 7)]+--+-- >>> majorityStyle commaPrecStyle [0,5e6,1e7,2e7]+-- ExponentStyle (Just 7)+majorityStyle :: (Double -> FormatStyle) -> [Double] -> FormatStyle+majorityStyle s xs = maj'+ where+ maj = fromMaybe CommaStyle (major (neutralExpStyle . s <$> xs))+ maj' = bool maj (ExponentStyle (fromMaybe Nothing expXs)) (maj == ExponentStyle Nothing)+ neutralExpStyle (ExponentStyle _) = ExponentStyle Nothing+ neutralExpStyle x = x++ expXs = major [x | (ExponentStyle x) <- s <$> xs]++major :: Ord a => [a] -> Maybe a+major xs = fst <$> listToMaybe (List.sortOn (Down . snd) $ Map.toList $ Map.fromListWith (+) ((,1 :: Integer) <$> xs))++-- | format a number according to a FormatStyle and significant figures+--+-- >>> format CommaStyle (Just 2) 1234+-- "1,200"+format :: FormatStyle -> Maybe Int -> Double -> Text+format fs n x = bool (go x) ("-" <> go (-x)) (x < 0)+ where+ go x' = case fs of+ DecimalStyle -> decimal n x'+ ExponentStyle n' -> exptWith n' n x'+ CommaStyle -> comma n x'+ FixedStyle n' -> fixed (Just n') x'+ PercentStyle -> percent commaSF n x'+ DollarStyle -> dollar commaSF n x'++-- | format a SigFig according to a style+--+-- >>> formatSF CommaStyle (toSigFig (Just 2) 1234)+-- "1,200"+--+-- >>> formatSF CommaStyle (SigFig SigFigPos 0 1)+-- "0"+-- >>> formatSF CommaStyle (SigFig SigFigPos 0 (-1))+-- "0.0"+formatSF :: FormatStyle -> SigFig -> Text+formatSF fs x = case fs of+ DecimalStyle -> decimalSF x+ ExponentStyle n' -> exptSFWith n' x+ CommaStyle -> commaSF x+ FixedStyle n -> fixed (Just n) (fromSigFig x)+ PercentStyle -> percentSF commaSF x+ DollarStyle -> dollarSF commaSF x++-- | Format between 0.001 and 1000000 using decimal style and exponential style outside this range.+-- -- >>> prec (Just 2) 0.00234 -- "0.0023" --@@ -296,94 +452,144 @@ -- >>> prec (Just 2) 1234567 -- "1.2e6" prec :: Maybe Int -> Double -> Text-prec n x = bool (go x) ("-" <> go (-x)) (x < 0)- where- go x' =- bool- ( bool- (decimal n x')- (expt n x')- (x' > 1e6)- )- (expt n x')- (x' < 0.001 && (x /= 0))+prec n x = format (precStyle x) n x -- | Format using comma separators for numbers above 1,000 but below 1 million, otherwise use prec style. ----- >>> comma (Just 3) 1234+-- >>> commaPrec (Just 3) 1234 -- "1,230"-comma :: Maybe 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 =- uncurry (<>)- . first (Text.reverse . Text.intercalate "," . Text.chunksOf 3 . Text.reverse)- . Text.breakOn "."+--+-- >>> commaPrec (Just 3) 1234567+-- "1.23e6"+commaPrec :: Maybe Int -> Double -> Text+commaPrec n x = format (commaPrecStyle x) n x --- | Format as dollars, always using comma notation+-- | Consistently format a list of numbers ----- >>> dollar (Just 3) 1234--- "$1,230"+-- >>> formats True precStyle (Just 1) [0,0.5,1,2]+-- ["0.0","0.5","1.0","2.0"] ----- >>> dollar (Just 2) 0.01234--- "$0.012"-dollar :: Maybe Int -> Double -> Text-dollar n x- | x < 0 = "-" <> dollar n (-x)- | otherwise = "$" <> comma n x---- | Format as a percentage using decimal style.+-- >>> formats False precStyle (Just 1) $ ((-1)*) <$> [0,0.5,1,2]+-- ["0.0","-0.5","-1.0","-2.0"] ----- >>> percent (Just 2) 0.001234--- "0.12%"-percent :: Maybe Int -> Double -> Text-percent n x = (<> "%") $ decimal n (100 * x)+-- >>> formats True commaPrecStyle (Just 1) $ ((-1000)*) <$> [0,0.5,1,2]+-- [" 0"," -500","-1,000","-2,000"]+--+-- >>> formats True commaPrecStyle (Just 1) $ ((1e6)*) <$> [0,0.5,1,2]+-- [" 0"," 500,000","1,000,000","2,000,000"]+--+-- >>> formats True commaPrecStyle (Just 1) $ ((1e6)*) <$> [0.9,2,3]+-- ["0.9e6","2.0e6","3.0e6"]+--+-- >>> formats True commaPrecStyle (Just 1) $ ((1e-6)*) <$> [0,0.5,1,2]+-- ["0.0e-6","0.5e-6","1.0e-6","2.0e-6"]+--+-- >>> formats True commaPrecStyle (Just 1) $ ((1e-3)*) <$> [0,0.5,1,2]+-- ["0.0000","0.0005","0.0010","0.0020"]+formats ::+ -- | left pad to the largest text length+ Bool ->+ -- | style+ (Double -> FormatStyle) ->+ -- | significant figures requested+ Maybe Int ->+ -- | list of numbers+ [Double] ->+ [Text]+formats lpad s n0 xs = bool fsigs (lpads fsigs) lpad+ where+ sigs = toSigFig n0 <$> xs+ minexp = minimum (sfExponent <$> filter (\x -> sfFigures x /= 0) sigs)+ sigs' = (\x -> bool (incSigFig (sfExponent x - minexp) x) (x {sfExponent = min 1 minexp}) (sfFigures x == 0)) <$> sigs+ maj = majorityStyle s (fromSigFig <$> sigs')+ fsigs = formatSF maj <$> sigs' -precision_ :: (Maybe Int -> Double -> Text) -> Int -> [Double] -> [Text]-precision_ f n0 xs =- precLoop n0 xs+lpads :: [Text] -> [Text]+lpads ts = (\x -> mconcat (replicate (maxl - Text.length x) " ") <> x) <$> ts where- precLoop n xs' =- let s = f (Just n) <$> xs'- in bool (precLoop (1 + n) xs') s (s == nubOrd s || n > 4)+ maxl = maximum $ Text.length <$> ts -- | Provide formatted text for a list of numbers so that they are just distinguished. ----- For example, __@precision comma (Just 2)@__ means use as much significant figures as is needed for the numbers to be distinguished on rendering, but with at least 2 significant figures.------ The difference between this and __@fmap (comma (Just 2))@__ can be seen in these examples:+-- For example, __@distinguish 4 commaPrecStyle (Just 2)@__ means use as much significant figures as is needed for the numbers to be distinguished on rendering (up to 4+2=6), but with at least 2 significant figures. ----- >>> precision comma (Just 2) [0,1,1.01,1.02,1.1,1.2]--- ["0.00","1.00","1.01","1.02","1.10","1.20"]+-- The difference between this and 'formats' can be seen in these examples: ----- >>> fmap (comma (Just 2)) [0,1,1.01,1.02,1.1,1.2]+-- >>> formats True commaPrecStyle (Just 2) [0,1,1.01,1.02,1.1,1.2] -- ["0.0","1.0","1.0","1.0","1.1","1.2"]-precision :: (Maybe Int -> Double -> Text) -> Maybe Int -> [Double] -> [Text]-precision f n xs =+--+-- >>> distinguish 4 True commaPrecStyle (Just 2) [0,1,1.01,1.02,1.1,1.2]+-- ["0.00","1.00","1.01","1.02","1.10","1.20"]+distinguish ::+ -- | maximum number of iterations+ Int ->+ -- | left pad to the largest text length+ Bool ->+ -- | style+ (Double -> FormatStyle) ->+ -- | significant figures requested+ Maybe Int ->+ -- | list of numbers+ [Double] ->+ [Text]+distinguish maxi pad f n xs = case n of- Nothing -> f Nothing <$> xs- Just n' -> precision_ f n' xs+ Nothing -> formats pad f Nothing xs+ Just n0 -> loop n0 xs+ where+ loop n' xs' =+ let s = formats pad f (Just n') xs'+ in bool (loop (1 + n') xs') s (s == nubOrd s || n' > maxi) --- | Consistently format a list of numbers via using 'precision'.+-- | Wrapper for the various formatting options. ----- >>> formatNs defaultFormatN [0,1,1.01,1.02,1.1,1.2]+-- >>> defaultFormatN+-- FormatN {fstyle = FSCommaPrec, sigFigs = Just 2, addLPad = True}+data FormatN = FormatN {fstyle :: FStyle, sigFigs :: Maybe Int, addLPad :: Bool} deriving (Eq, Show, Generic)++-- | The official FormatN+defaultFormatN :: FormatN+defaultFormatN = FormatN FSCommaPrec (Just 2) True++-- | run a 'FormatN'+--+-- >>> formatN defaultFormatN 1234+-- "1,200"+formatN :: FormatN -> Double -> Text+formatN (FormatN FSDecimal sf _) x = format DecimalStyle sf x+formatN (FormatN (FSExponent n) sf _) x = format (ExponentStyle n) sf x+formatN (FormatN FSComma sf _) x = format CommaStyle sf x+formatN (FormatN (FSFixed n) sf _) x = format (FixedStyle n) sf x+formatN (FormatN FSPercent sf _) x = format PercentStyle sf x+formatN (FormatN FSDollar sf _) x = format DollarStyle sf x+formatN (FormatN FSPrec sf _) x = format (precStyle x) sf x+formatN (FormatN FSCommaPrec sf _) x = format (commaPrecStyle x) sf x+formatN (FormatN FSNone _ _) x = pack (show x)++-- | Consistently format a list of numbers via using 'distinguish'.+--+-- >>> formatNs 4 defaultFormatN [0,1,1.01,1.02,1.1,1.2] -- ["0.00","1.00","1.01","1.02","1.10","1.20"]-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+formatNs :: Int -> FormatN -> [Double] -> [Text]+formatNs maxi (FormatN FSDecimal sf pad) x = distinguish maxi pad (const DecimalStyle) sf x+formatNs maxi (FormatN (FSExponent n) sf pad) x = distinguish maxi pad (const (ExponentStyle n)) sf x+formatNs maxi (FormatN FSComma sf pad) x = distinguish maxi pad (const CommaStyle) sf x+formatNs maxi (FormatN (FSFixed n) sf pad) x = distinguish maxi pad (const (FixedStyle n)) sf x+formatNs maxi (FormatN FSPercent sf pad) x = distinguish maxi pad (const PercentStyle) sf x+formatNs maxi (FormatN FSDollar sf pad) x = distinguish maxi pad (const DollarStyle) sf x+formatNs maxi (FormatN FSPrec sf pad) x = distinguish maxi pad precStyle sf x+formatNs maxi (FormatN FSCommaPrec sf pad) x = distinguish maxi pad commaPrecStyle sf x+formatNs _ (FormatN FSNone _ pad) x = bool id lpads pad $ pack . show <$> x --- | Format with the shorter of show and formatN.-showOr :: FormatN -> Double -> Text-showOr f x = bool (bool f' (pack s') (Text.length (pack s') < Text.length f')) "0" (x < 1e-6 && x > -1e-6)+-- | Format with the shorter of show and a style.+--+-- >>> format (ExponentStyle Nothing) Nothing 0+-- "0e0"+--+-- >>> formatOrShow (ExponentStyle Nothing) Nothing 0+-- "0"+formatOrShow :: FormatStyle -> Maybe Int -> Double -> Text+formatOrShow f n x = bool (bool f' (pack s') (Text.length (pack s') < Text.length f')) "0" (x < 1e-6 && x > -1e-6) where- f' = formatN f x+ f' = format f n x s' = show x