File: FloatingHex.hs

package info (click to toggle)
haskell-floatinghex 0.5-4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 76 kB
  • sloc: haskell: 90; makefile: 2
file content (158 lines) | stat: -rw-r--r-- 6,226 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
-------------------------------------------------------------------------
-- |
-- Module      :  Data.Numbers.FloatingHex
-- Copyright   :  (c) Levent Erkok
-- License     :  BSD3
-- Maintainer  :  erkokl@gmail.com
-- Stability   :  experimental
--
-- Reading/Writing hexadecimal floating-point numbers.
--
-- See: <http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf>, pages 57-58.
-- We slightly diverge from the standard and do not allow for the "floating-suffix,"
-- as the type inference of Haskell makes this unnecessary.
-----------------------------------------------------------------------------
module Data.Numbers.FloatingHex (
        -- ** QuasiQuoting
        hf
        -- ** Reading hex-floats
        , FloatingHexReader(..)
        -- ** Showing hex-floats
        , showHFloat
        ) where

import Data.Char  (toLower)
import Data.Ratio ((%))
import Numeric    (showHex)
import GHC.Float

import qualified Language.Haskell.TH.Syntax as TH
import           Language.Haskell.TH.Quote

-- | Due to intricacies of conversion between
-- @Float@ and @Double@ types (see <http://ghc.haskell.org/trac/ghc/ticket/3676>), we explicitly introduce
-- a class to do the reading properly.
class RealFloat a => FloatingHexReader a where
   -- | Convert a hex-float from a string, if possible.
   readHFloat :: String -> Maybe a

-- | The Float instance
instance FloatingHexReader Float where
   readHFloat s = double2Float `fmap` readHFloatAsDouble s

-- | The Double instance
instance FloatingHexReader Double where
   readHFloat = readHFloatAsDouble

-- | Read a float in hexadecimal binary format. Supports negative numbers, and nan/infinity as well.
-- For regular usage, the quasiquoter (`hf`) should be employed. But this function can be handy for
-- programmatic interfaces.
readHFloatAsDouble :: String -> Maybe Double
readHFloatAsDouble = cvt
  where cvt ('-' : cs) = ((-1) *) `fmap` go cs
        cvt cs         = go cs

        go "NaN"      = Just $ 0/0
        go "Infinity" = Just $ 1/0
        go cs         = parseHexFloat cs

-- | Turn a hexadecimal float to an internal double, if parseable. Does not support the leading
-- '-' bit, although it does allow a leading +. (The former is best done out of the quasiquote,
-- since TH does not cannot represent negative 0! See <https://ghc.haskell.org/trac/ghc/ticket/13124>
-- for why we avoid this here.)
parseHexFloat :: String -> Maybe Double
parseHexFloat = goS . map toLower
  where goS ('+':rest) = go0 rest
        goS cs         = go0 cs

        go0 ('0':'x':rest) = go1 rest
        go0 _              = Nothing

        go1 cs = case break (== 'p') cs of
                   (pre, 'p':'+':d) -> go2 pre d
                   (pre, 'p':    d) -> go2 pre d
                   _                -> Nothing

        go2 cs = case break (== '.') cs of
                   (pre, '.':post) -> construct pre post
                   _               -> construct cs  ""

        rd :: Read a => String -> Maybe a
        rd s = case reads s of
                 [(x, "")] -> Just x
                 _         -> Nothing

        construct pre post d = do a <- rd $ "0x" ++ pre ++ post
                                  e <- rd d
                                  return $ val a (length post) e

        val :: Integer -> Int -> Integer -> Double
        val a b e
          | e > 0 = fromRational $ (top * power) % bot
          | True  = fromRational $ top % (power * bot)
          where top, bot, power :: Integer
                top   = a
                bot   = 16 ^ b
                power =  2 ^ abs e

-- | A quasiquoter for hexadecimal floating-point literals.
-- See: <http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf>, pages 57-58.
-- We slightly diverge from the standard and do not allow for the "floating-suffix,"
-- as the type inference of Haskell makes this unnecessary.
--
-- Example:
--
--  > {-# LANGUAGE QuasiQuotes #-}
--  > import Data.Numbers.FloatingHex
--  >
--  > f :: Double
--  > f = [hf|0x1.f44abd5aa7ca4p+25|]
--
--  With these definitions, @f@ will be equal to the number @6.5574266708245546e7@
hf :: QuasiQuoter
hf = QuasiQuoter { quoteExp  = q
                 , quotePat  = p
                 , quoteType = error "Unexpected hexadecimal float in a type context"
                 , quoteDec  = error "Unexpected hexadecimal float in a declaration context"
                 }
   where q :: String -> TH.Q TH.Exp
         q s = case parseHexFloat s of
                  Just d  -> TH.lift d
                  Nothing -> fail $ "Invalid hexadecimal floating point number: |" ++ s ++ "|"

         p :: String -> TH.Q TH.Pat
         p s = case parseHexFloat s of
                  Just d  -> return (TH.LitP (TH.RationalL (toRational d)))
                  Nothing -> fail $ "Invalid hexadecimal floating point number: |" ++ s ++ "|"

-- | Show a floating-point value in the hexadecimal format, similar to the @%a@ specifier in C's printf.
--
-- >>> showHFloat (212.21 :: Double) ""
-- "0x1.a86b851eb851fp7"
-- >>> showHFloat (-12.76 :: Float) ""
-- "-0x1.9851ecp3"
-- >>> showHFloat (-0 :: Double) ""
-- "-0x0p+0"
showHFloat :: RealFloat a => a -> ShowS
showHFloat = showString . fmt
  where fmt x | isNaN x                   = "NaN"
              | isInfinite x              = (if x < 0 then "-" else "") ++ "Infinity"
              | x < 0 || isNegativeZero x = '-' : cvt (-x)
              | True                      =       cvt x

        cvt x
          | x == 0 = "0x0p+0"
          | True   = case floatToDigits 2 x of
                       r@([], _) -> error $ "Impossible happened: showHFloat: " ++ show r
                       (d:ds, e) -> "0x" ++ show d ++ frac ds ++ "p" ++ show (e-1)

        -- Given binary digits, convert them to hex in blocks of 4
        -- Special case: If all 0's, just drop it.
        frac digits
          | all (== 0) digits = ""
          | True              = "." ++ hex digits
          where hex ds
                  | null ds       = ""
                  | length ds < 4 = hex (take 4 (ds ++ repeat 0))
                  | True          = let (d, r) = splitAt 4 ds in hexDigit d ++ hex r
                hexDigit d        = showHex (foldl (\a b -> 2*a+b) 0 d) ""