SH:IsString Instances:

Writing Integers in English

Another worthless use of IsString.

Just grab the file, IntegerIsString.hs. This lets you write integers in English.

$ ghci
GHCi, version 6.8.2: http://www.haskell.org/ghc/  :? for help
Loading package base ... linking ... done.
Prelude> :l IntegerIsString
[1 of 1] Compiling IntegerIsString  ( IntegerIsString.hs, interpreted )
Ok, modules loaded: IntegerIsString.
*IntegerIsString> :set -XOverloadedStrings
*IntegerIsString> "two billion one" - "negative fifty-nine thousand"
2000059001

Aw, heck, why don't I put the source on the page, too.

{-# LANGUAGE ParallelListComp #-}

module IntegerIsString where

import Control.Monad (mplus, guard, ap)
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import Data.String
import Text.ParserCombinators.Parsec

-- We're interested in making an IsString instance for Integers that
-- lets us write the Strings in English.  For example, we want to
-- write ("twenty-three" + "five") and get 28.

instance IsString Integer where
    fromString s = fromMaybe invalid (readEnglishInteger s)
        where invalid = error "IntegerIsString.fromString: invalid string"

-- Our implementation of fromString assumes that 'readEnglishInteger'
-- has the type, String -> Maybe Integer.  It is now up to us to
-- implement that function.

-- So we're interested in making a function that parses a string to
-- read an integer.  In other words, a function that takes in "one
-- thousand twenty-nine" and returns (Just 1029).  We'll use a Parsec
-- parser to cleanly do the dirty work; we'll just call the parser and
-- pass on the result.

readEnglishInteger :: String -> Maybe Integer
readEnglishInteger s = case parse number "" s of
                         Left err -> Nothing
                         Right x  -> Just x



number :: Parser Integer
number = try zero <|> nonzero

zero :: Parser Integer
zero = do word "zero"
          noMoreWords
          return 0

noMoreWords :: Parser ()
noMoreWords = do optional spaces
                 eof

word :: String -> Parser ()
word s = try $ do optional spaces
                  string s
                  lookAheadEndOfWord

lookAheadEndOfWord :: Parser ()
lookAheadEndOfWord = lookAhead $ (eof <|> do { space ; return () })




nonzero :: Parser Integer
nonzero = maybeNegate `ap` positiveNumber

maybeNegate :: Parser (Integer -> Integer)
maybeNegate = option id $ do { word "negative" ; return negate }


positiveNumber :: Parser Integer
positiveNumber = decliningPeriods 0 (1 + maxPeriod)

somePeriod :: Parser (Integer, Integer)
somePeriod = do h <- hundredsPlace
                t <- twoDigit
                guard (h > 0  ||  t > 0)
                p <- periodName
                return ((h * 100 + t) * (1000 ^ p), p)

-- Periods must come in decreasing order.  You can't say "three
-- million two billion five".
decliningPeriods :: Integer -> Integer -> Parser Integer
decliningPeriods sum periodNum = finished <|> notFinished
    where finished   = try $ do noMoreWords
                                guard (sum /= 0) -- avoids empty strings
                                return sum
          notFinished = do (n, smallerPeriodNum) <- somePeriod
                           guard (smallerPeriodNum < periodNum)
                           decliningPeriods (n + sum) smallerPeriodNum

hundredsPlace :: Parser Integer
hundredsPlace = option 0 (try $ do d <- digitName
                                   word "hundred"
                                   return d)

twoDigit :: Parser Integer
twoDigit = lessThanNineteen <|> moreThanNineteen

lessThanNineteen = tryAll names [1..19]
    where names = digits ++ [ "ten", "eleven", "twelve", "thirteen"
                            , "fourteen", "fifteen", "sixteen"
                            , "seventeen", "eighteen", "nineteen"
                            ]

moreThanNineteen = do optional spaces
                      t <- tightTensDigit
                      u <- option 0 tensDigitTail
                      return (10 * t + u)
    where tightTensDigit = choice [ do try (string s)
                                       return n
                                    | s <- tens
                                    | n <- [2..9]
                                  ]
          tens = [ "twenty", "thirty", "forty", "fifty"
                 , "sixty", "seventy", "eighty", "ninety"
                 ]
          tensDigitTail = (do lookAheadEndOfWord
                              return 0)
                          <|>
                          (do char '-'
                              notFollowedBy space
                              digitName)

-- This combinator tries a bunch of words and returns the value in the
-- parallel list matching the word it finds
tryAll :: [String] -> [a] -> Parser a
tryAll strs ns = try $ choice [ do word s
                                   return n
                                | s <- strs
                                | n <- ns
                              ]


digitName = tryAll digits [1..9]

digits = [ "one", "two", "three", "four", "five"
         , "six", "seven", "eight", "nine"
         ]

periodName = tryAll periods [1..]
             <|>
             (do noMoreWords
                 return 0)

periods = [ "thousand", "million", "billion", "trillion", "quadrillion"
          , "quintillion", "sextillion", "septillion", "octillion"
          , "nonillion", "decillion", "undecillion"
          ]

maxPeriod = toInteger (length periods)



Copyright © 2009-2010 Sam Hughes