packages feed

babynf-0.1.0.2: lib/Data/BAByNF/Util/Ascii.hs

module Data.BAByNF.Util.Ascii
    ( lowerAlphaFirst
    , lowerAlphaLast
    , upperAlphaFirst
    , upperAlphaLast
    , rangedCompare
    , AlphaClass (..)
    , classifyAlpha
    , lowerToUpperUnsafe
    , lowerToUpper
    , eqNoCase
    , eqNoCaseSeq
    , eqNoCaseBS
    , fromChar
    , fromCharOrNull
    , bs
    , parseHex
    , toHexDigit
    , bsToHexDigit
    , toHexSeq
    , toDecimalDigit
    , bsToDecimalDigit
    , toBinaryDigit
    , bsToBinaryDigit
    , stringAsBytesUnsafe
    , parseCaseInsensitive
    , parseCaseSensitive
    ) where

import Data.Functor ((<&>))
import Data.Char qualified as Char
import Data.Maybe qualified as Maybe
import Data.Word (Word8)
import Data.List qualified as List

import Data.ByteString (ByteString)
import Data.ByteString qualified as ByteString
import Data.ByteString.Char8 qualified as ByteString.Char8

import Data.Attoparsec.ByteString qualified as Attoparsec.ByteString

import Data.BAByNF.Util.Binary qualified as Binary
import Data.BAByNF.Util.Decimal qualified as Decimal
import Data.BAByNF.Util.Hex qualified as Hex
import Control.Applicative ((<|>))

lowerAlphaFirst :: Word8
lowerAlphaFirst = 97

lowerAlphaLast :: Word8
lowerAlphaLast = 122

upperAlphaFirst :: Word8
upperAlphaFirst = 65

upperAlphaLast :: Word8
upperAlphaLast = 90


rangedCompare :: Ord a =>  a -> a -> a -> Ordering
rangedCompare lo hi x
  | x < lo = LT
  | x > hi = GT
  | otherwise = EQ

data AlphaClass = UpperAlpha | LowerAlpha deriving (Eq, Show)

classifyAlpha :: Word8 -> Maybe AlphaClass
classifyAlpha a = case rangedCompare upperAlphaFirst upperAlphaLast a of
    LT -> Nothing
    EQ -> Just UpperAlpha
    GT -> case rangedCompare lowerAlphaFirst lowerAlphaLast a of
        EQ -> Just LowerAlpha
        _ -> Nothing

lowerToUpperUnsafe :: Word8 -> Word8
lowerToUpperUnsafe a = a - lowerUpperDiff
    where lowerUpperDiff = lowerAlphaFirst - upperAlphaFirst

lowerToUpper :: Word8 -> Maybe Word8
lowerToUpper a = do
    alphaClass <- classifyAlpha a
    if alphaClass == LowerAlpha
        then Just (lowerToUpperUnsafe a)
        else Nothing

eqNoCase :: Word8 -> Word8 -> Bool
eqNoCase a b =
    case classifyAlpha a of
        Nothing -> a == b
        Just ac -> case classifyAlpha b of
           Nothing -> False
           Just bc -> if bc == ac
               then a == b
               else if ac == UpperAlpha
                   then a == lowerToUpperUnsafe b
                   else b == lowerToUpperUnsafe a

eqNoCaseSeq :: [Word8] -> [Word8] -> Bool
eqNoCaseSeq [] [] = True
eqNoCaseSeq _ [] = False
eqNoCaseSeq [] _ = False
eqNoCaseSeq (x:xs) (y:ys) = eqNoCase x y && eqNoCaseSeq xs ys

eqNoCaseBS :: ByteString -> ByteString -> Bool
eqNoCaseBS a b = ByteString.length a == ByteString.length b && all eq' pairs
    where eq' = uncurry eqNoCase
          pairs = ByteString.zip a b

fromChar :: Char -> Maybe Word8
fromChar ch =
    if Char.isAscii ch
        then Just (fromIntegral (Char.ord ch))
        else Nothing

fromCharOrNull :: Char -> Word8
fromCharOrNull ch = Maybe.fromMaybe 0 (fromChar ch)

bs :: Char -> ByteString
bs ch = ByteString.singleton (fromCharOrNull ch)

parseHex :: (Integral a) => ByteString -> Maybe a
parseHex s = fmap (Hex.toNum . Hex.Seq) (mapM toHexDigit (ByteString.unpack s))

toHexDigit :: Word8 -> Maybe Hex.Digit
toHexDigit w
    | w >= 48 && w <= 57 = Hex.fromVal (w - 48)
    | w >= 97 && w <= 102 = Hex.fromVal (w - 97 + 10)
    |  w >= 65 && w <= 70 = Hex.fromVal (w - 65 + 10)
    | otherwise = Nothing

bsToHexDigit :: ByteString -> Maybe Hex.Digit
bsToHexDigit b =
    case ByteString.uncons b of
        Just (w, t) | ByteString.null t -> toHexDigit w
                    | otherwise -> Nothing
        _ -> Nothing
toHexSeq :: ByteString -> Maybe Hex.Seq
toHexSeq b = toHexDigs b <&> Hex.Seq
    where toHexDigs x = ByteString.uncons x >>=
            \(h, rest) ->  toHexDigit h >>=
            \hexdig -> if ByteString.null rest
                then Just [hexdig]
                else toHexDigs rest >>= \hexdigs -> Just (hexdig:hexdigs)

toDecimalDigit :: Word8 -> Maybe Decimal.Digit
toDecimalDigit w
    | w >= 48 && w <= 57 = Decimal.fromVal (w - 48)
    | otherwise = Nothing

bsToDecimalDigit :: ByteString -> Maybe Decimal.Digit
bsToDecimalDigit b =
    case ByteString.uncons b of
        Just (w, t) | ByteString.null t -> toDecimalDigit w
                    | otherwise -> Nothing
        _ -> Nothing

toBinaryDigit :: Word8 -> Maybe Binary.Digit
toBinaryDigit w
    | w `elem` [48, 49] = Binary.fromVal (w - 48)
    | otherwise = Nothing

bsToBinaryDigit :: ByteString -> Maybe Binary.Digit
bsToBinaryDigit b =
    case ByteString.uncons b of
        Just (w, t) | ByteString.null t -> toBinaryDigit w
                    | otherwise -> Nothing
        _ -> Nothing

stringAsBytesUnsafe :: String -> ByteString
stringAsBytesUnsafe s =
    case List.find (not . Char.isAscii) s of
        Just _ -> error "string contains non-ascii characters"
        Nothing -> ByteString.Char8.pack s

parseCaseInsensitive :: ByteString -> Attoparsec.ByteString.Parser ByteString
parseCaseInsensitive b = Attoparsec.ByteString.take (ByteString.length b)
    >>= \b' -> if b `eqNoCaseBS` b' then return b' else fail "case insensitive match fail"

parseCaseSensitive :: ByteString -> Attoparsec.ByteString.Parser ByteString
parseCaseSensitive b = Attoparsec.ByteString.string b <|> fail "case sensitive match fail"