packages feed

haskeem-0.7.12: WriteNumber.hs

{- Copyright 2008 Uwe Hollerbach <uh@alumni.caltech.edu>
Portions of this were derived from Jonathan Tang's haskell
tutorial "Write yourself a scheme in 48 hours" and are thus
Copyright Jonathan Tang (but there isn't much of his stuff left).

This file is part of haskeem.
haskeem is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

haskeem is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with haskeem; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

$Id: writenumber.hs,v 1.15 2009-08-06 05:05:15 uwe Exp $ -}

module WriteNumber (writeNum, ilogb) where
import Prelude
import Numeric
import Data.Ratio
import Maybe
import Control.Monad.Error as CME

import LispData

-- Generate the base prefix for a number. This is included explicitly in all
-- numbers generated by writeNum: that is contrary to what R6RS says; however,
-- I don't understand the rationale: what's the point of having "deadbeef"
-- floating around if you don't mark it as a number (if it is one)?

prefix :: Integer -> String
prefix 2 = "#b"
prefix 8 = "#o"
prefix 10 = ""    -- implicit decimal, or "#d" if explicit prefix is desired
prefix 16 = "#x"

-- Exponent marker for floating-point numbers in scientific notation:
-- if we just use 'e' all the time, it makes base-16 numbers ambiguous,
-- so write 'x' for base-16. The parser understands this.

emark :: Integer -> String
emark n | n == 16   = "x"
        | otherwise = "e"

sign :: (Num a, Ord a) => a -> String
sign n | n < 0     = "-"
       | otherwise = ""    -- or "+" if explicit sign is desired

sign2 :: Integer -> Integer -> String
sign2 n d | d == 0    = sign n		-- to distinguish -1/0 from 1/0
          | otherwise = sign (n*d)

d2c :: Int -> Char
d2c d = ("0123456789abcdef" !! d)

shi :: Integer -> Integer -> String
shi b n = showIntAtBase b d2c n ""

-- fast integer base-b log: this is exported for use in the library
-- as well as here

ilogb :: Integer -> Integer -> Integer
ilogb b n | n < 0      = ilogb b (- n)
          | n < b      = 0
          | otherwise  = (up 1) - 1
  where up a = if n < (b ^ a)
                  then bin (quot a 2) a
                  else up (2*a)
        bin lo hi = if (hi - lo) <= 1
                       then hi
                       else let av = quot (lo + hi) 2
                            in if n < (b ^ av)
                                  then bin lo av
                                  else bin av hi

-- A small limitation: some of the lengths of strings are Int not Integer,
-- so all of the specified-precision stuff is is limited to somewhere around
-- MAXINT decimal places. That seems ok for now... even the minimum Int
-- size of 2^29 is pretty damned big for a number of digits.

writeInt :: Integer -> Integer -> Maybe Integer -> String
writeInt n b p =
  let pval = fromJust p
      nval = (shi b (abs n))
      hdr = (prefix b) ++ (sign n)
  in if isNothing p
        then hdr ++ nval
        else if pval >= 0
                then hdr ++ nval ++ "." ++ (replicate (fromInteger pval) '0')
                else writeRatE (fromInteger n) b (-pval)

-- The non-finite rationals get treated specially: they get written
-- in rational format regardless of whether we specified a precision

isSpRat :: Rational -> Bool
isSpRat n = (n == myRatNaN) || (n == myRatPInf) || (n == myRatNInf)

writeSpRat :: Rational -> [Char]
writeSpRat n | n == myRatNaN    = "0/0"
             | n == myRatPInf   = "1/0"
             | n == myRatNInf   = "-1/0"

-- this is pretty complicated, but it's all pure, and because haskell
-- is lazy only those parts which are needed will get evaluated

writeRatF :: Rational -> Integer -> Maybe Integer -> String
writeRatF num b p =
  let n = numerator num
      d = denominator num
      an = (abs n)
      ad = (abs d)
      pval = fromJust p
      nval = (abs num)
      (ni, nr) = quotRem an ad
      pfrac = b ^ pval
      nf = nr * pfrac
      (nj, ns) = quotRem nf ad
      nt = 2*ns
      ng = if (nt < ad) || ((nt == ad) && (even nj)) then nj else nj + 1
      (carry, remder) = quotRem ng pfrac
      intp = shi b (ni + carry)
      fnz = shi b remder
      len = fromInteger (pval - (toInteger (length fnz)))
      fracp = (replicate len '0') ++ fnz
      hdr = (prefix b) ++ (sign2 n d)
  in if isNothing p
        then hdr ++ (shi b an) ++ "/" ++ (shi b ad)
        else if pval > 0
                then hdr ++ intp  ++ "." ++ fracp
                else if pval == 0
                        then hdr ++ (shi b (round nval)) ++ "."
                        else writeRatE num b (-pval)

-- Normalize a rational number n such that it's in the half-open interval
-- [1, b), and return the scaled number and the logarithmic scale factor
-- required to get it there. The funky stuff with (sn2,lb2) etc is to
-- eliminate possible errors due to inaccuracy in calculating logBase.

normTo :: Rational -> Integer -> (Rational, Integer)
normTo n b =
  let iln = ilogb 2 (numerator n)
      ild = ilogb 2 (denominator n)
      lb0 = (fromInteger (iln - ild))/(logBase 2.0 (fromInteger b))
      lb1 = toInteger (floor lb0)
      rb = fromInteger b
      sn1 = if lb1 >= 0
               then (n / (fromInteger (b ^ lb1)))
               else (n * (fromInteger (b ^ (-lb1))))
      (sn2, lb2) = if sn1 >= 1
                      then (sn1, lb1)
                      else (sn1 * rb, lb1 - 1)
  in if sn2 < rb
        then (sn2, lb2)
        else (sn2 / rb, lb2 + 1)

writeRatE :: Rational -> Integer -> Integer -> String
writeRatE num b p =
  let (sn, se) = normTo (abs num) b
      snr = sn/(fromInteger b)
      ser = se+1
      str = (writeRatF ((signum num)*sn) b (Just (abs p))) ++ (ssuf se)
      strr = (writeRatF ((signum num)*snr) b (Just (abs p))) ++ (ssuf ser)
  in if num == 0
        then (writeRatF (0%1) b (Just (abs p))) ++ (ssuf 0)
        else if noround str
                then str
                else strr
  where ssuf e = (emark b) ++ (sign e) ++ (shi b (abs e))
        noround s = chek (takeWhile (/= '.') s)
        chek ('#':_:rest) = chek rest
        chek ('-':rest) = chek rest
        chek "10" = False
        chek _ = True

-- The non-finite floating-point numbers get treated specially.

isSpFlt :: RealFloat a => a -> Bool
isSpFlt n = (isNaN n) || (isInfinite n)

writeSpFlt :: RealFloat a => a -> String
writeSpFlt n | n > 0       = "+inf.0"
             | n < 0       = "-inf.0"
             | otherwise   = "+nan.0"

showFltAtBase :: Integer -> Double -> String
showFltAtBase b x =
  let ((f:r),e) = floatToDigits b x
  in (d2c f):("." ++ (map d2c r) ++ (emark b) ++
     (sign (e - 1)) ++ (shi b (toInteger (abs (e - 1)))))

writeFlt :: Double -> Integer -> Maybe Integer -> String
writeFlt n b p =
  if isNothing p
     then (prefix b) ++ (sign n) ++ (showFltAtBase b (abs n))
     else writeRatF (toRational n) b p

goodBase :: Integer -> Bool
goodBase b = (b == 2 || b == 8 || b == 10 || b == 16)

badBase b =
  throwError (Default ("bad base " ++ (show b) ++ " in number->string"))

writeNum :: [LispVal] -> ThrowsError LispVal

writeNum [IntNumber n] = return (String (writeInt n 10 Nothing))
writeNum [IntNumber n, IntNumber b] =
  if goodBase b
     then return (String (writeInt n b Nothing))
     else badBase b
writeNum [IntNumber n, IntNumber b, IntNumber p] =
  if goodBase b
     then return (String (writeInt n b (Just p)))
     else badBase b

writeNum [RatNumber n] = writeNum [RatNumber n, IntNumber 10]
writeNum [RatNumber n, IntNumber b] =
  if isSpRat n
     then return (String (writeSpRat n))
     else if goodBase b
             then return (String (writeRatF n b Nothing))
             else badBase b
writeNum [RatNumber n, IntNumber b, IntNumber p] =
  if isSpRat n
     then return (String (writeSpRat n))
     else if goodBase b
             then return (String (writeRatF n b (Just p)))
             else badBase b

writeNum [FltNumber n] = writeNum [FltNumber n, IntNumber 10]
writeNum [FltNumber n, IntNumber b] =
  if isSpFlt n
     then return (String (writeSpFlt n))
     else if goodBase b
             then return (String (writeFlt n b Nothing))
             else badBase b
writeNum [FltNumber n, IntNumber b, IntNumber p] =
  if isSpFlt n
     then return (String (writeSpFlt n))
     else if goodBase b
             then return (String (writeFlt n b (Just p)))
             else badBase b

writeNum badArgList =
  if length badArgList <= 3
     then throwError (TypeMismatch "number->string" "number" (badArgList !! 0))
     else throwError (NumArgs "number->string" 3 badArgList)