dictionaries 0.1.0.1 → 0.2.0
raw patch · 7 files changed
+705/−359 lines, 7 filesdep +QuickCheckdep +criteriondep +deepseqdep ~basedep ~bytestringdep ~containersnew-component:exe:benchPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, criterion, deepseq, dictionaries, hspec, random, random-shuffle, tagged
Dependency ranges changed: base, bytestring, containers, directory, exceptions, filepath, text, time, transformers
API changes (from Hackage documentation)
- NLP.Dictionary.StarDict: DictionaryNotFound :: FilePath -> StarDictException
- NLP.Dictionary.StarDict: IfoFile :: ByteString -> String -> Text -> Int -> Int -> Maybe Int -> Maybe Int -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe UTCTime -> Maybe String -> Maybe String -> IfoFile
- NLP.Dictionary.StarDict: IndexNotFound :: FilePath -> StarDictException
- NLP.Dictionary.StarDict: StarDict :: IfoFile -> Index -> FilePath -> Get [DataEntry] -> Renderer -> StarDict
- NLP.Dictionary.StarDict: WrongIfoFormat :: FilePath -> String -> StarDictException
- NLP.Dictionary.StarDict: WrongIndexFormat :: FilePath -> String -> StarDictException
- NLP.Dictionary.StarDict: [ifoAuthor] :: IfoFile -> Maybe Text
- NLP.Dictionary.StarDict: [ifoBookName] :: IfoFile -> Text
- NLP.Dictionary.StarDict: [ifoDate] :: IfoFile -> Maybe UTCTime
- NLP.Dictionary.StarDict: [ifoDescription] :: IfoFile -> Maybe Text
- NLP.Dictionary.StarDict: [ifoDictType] :: IfoFile -> Maybe String
- NLP.Dictionary.StarDict: [ifoEmail] :: IfoFile -> Maybe Text
- NLP.Dictionary.StarDict: [ifoIdxFileSize] :: IfoFile -> Int
- NLP.Dictionary.StarDict: [ifoIdxOffsetBits] :: IfoFile -> Maybe Int
- NLP.Dictionary.StarDict: [ifoMagicData] :: IfoFile -> ByteString
- NLP.Dictionary.StarDict: [ifoSameTypeSequence] :: IfoFile -> Maybe String
- NLP.Dictionary.StarDict: [ifoSynWordCount] :: IfoFile -> Maybe Int
- NLP.Dictionary.StarDict: [ifoVersion] :: IfoFile -> String
- NLP.Dictionary.StarDict: [ifoWebsite] :: IfoFile -> Maybe Text
- NLP.Dictionary.StarDict: [ifoWordCount] :: IfoFile -> Int
- NLP.Dictionary.StarDict: [sdDataParser] :: StarDict -> Get [DataEntry]
- NLP.Dictionary.StarDict: [sdDataPath] :: StarDict -> FilePath
- NLP.Dictionary.StarDict: [sdIfoFile] :: StarDict -> IfoFile
- NLP.Dictionary.StarDict: [sdIndex] :: StarDict -> Index
- NLP.Dictionary.StarDict: [sdRender] :: StarDict -> Renderer
- NLP.Dictionary.StarDict: checkDataFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m FilePath
- NLP.Dictionary.StarDict: checkFiles :: IfoFilePath -> [FilePath] -> IO (Maybe FilePath)
- NLP.Dictionary.StarDict: checkGZFiles :: IfoFilePath -> [FilePath] -> [FilePath] -> IO (Maybe (Either FilePath FilePath))
- NLP.Dictionary.StarDict: data IfoFile
- NLP.Dictionary.StarDict: data StarDict
- NLP.Dictionary.StarDict: data StarDictException
- NLP.Dictionary.StarDict: ifoDateFormat :: String
- NLP.Dictionary.StarDict: indexNumberParser :: IfoFile -> Get Int
- NLP.Dictionary.StarDict: instance GHC.Classes.Eq NLP.Dictionary.StarDict.DataEntry
- NLP.Dictionary.StarDict: instance GHC.Classes.Eq NLP.Dictionary.StarDict.IfoFile
- NLP.Dictionary.StarDict: instance GHC.Classes.Eq NLP.Dictionary.StarDict.StarDictException
- NLP.Dictionary.StarDict: instance GHC.Exception.Exception NLP.Dictionary.StarDict.StarDictException
- NLP.Dictionary.StarDict: instance GHC.Show.Show NLP.Dictionary.StarDict.DataEntry
- NLP.Dictionary.StarDict: instance GHC.Show.Show NLP.Dictionary.StarDict.IfoFile
- NLP.Dictionary.StarDict: instance GHC.Show.Show NLP.Dictionary.StarDict.StarDictException
- NLP.Dictionary.StarDict: instance NLP.Dictionary.Dictionary NLP.Dictionary.StarDict.StarDict
- NLP.Dictionary.StarDict: mkDataParser :: Maybe String -> Get [DataEntry]
- NLP.Dictionary.StarDict: readIfoFile :: (MonadThrow m, MonadIO m) => FilePath -> m IfoFile
- NLP.Dictionary.StarDict: readIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> Get Int -> m Index
- NLP.Dictionary.StarDict: type IfoFilePath = FilePath
- NLP.Dictionary.StarDict: type Index = Map Text (Int, Int)
- NLP.Dictionary.StarDict: type IndexEntry = (Text, (Int, Int))
- NLP.Dictionary.StarDict.InMemory: StarDict :: IfoFile -> Index -> ByteString -> Get [DataEntry] -> Renderer -> StarDict
- NLP.Dictionary.StarDict.InMemory: [sdDataParser] :: StarDict -> Get [DataEntry]
- NLP.Dictionary.StarDict.InMemory: [sdData] :: StarDict -> ByteString
- NLP.Dictionary.StarDict.InMemory: [sdIfoFile] :: StarDict -> IfoFile
- NLP.Dictionary.StarDict.InMemory: [sdIndex] :: StarDict -> Index
- NLP.Dictionary.StarDict.InMemory: [sdRender] :: StarDict -> Renderer
- NLP.Dictionary.StarDict.InMemory: data StarDict
- NLP.Dictionary.StarDict.InMemory: instance NLP.Dictionary.Dictionary NLP.Dictionary.StarDict.InMemory.StarDict
- NLP.Dictionary.StarDict.InMemory: mkDictionary :: (MonadThrow m, MonadIO m) => IfoFilePath -> Renderer -> m StarDict
+ NLP.Dictionary.StarDict: class (Dictionary d) => StarDict d
+ NLP.Dictionary.StarDict: getIfoFile :: StarDict d => d -> IfoFile
+ NLP.Dictionary.StarDict.Common: CJK :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: DictionaryNotFound :: FilePath -> StarDictException
+ NLP.Dictionary.StarDict.Common: HTML :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: IfoFile :: ByteString -> String -> Text -> Int -> Int -> Maybe Int -> Maybe Int -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe UTCTime -> Maybe String -> Maybe String -> IfoFile
+ NLP.Dictionary.StarDict.Common: IndexNotFound :: FilePath -> StarDictException
+ NLP.Dictionary.StarDict.Common: LocaleText :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: MediaWiki :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: Pango :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: Phonetics :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: Picture :: ByteString -> DataEntry
+ NLP.Dictionary.StarDict.Common: PowerWord :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: Reserved :: ByteString -> DataEntry
+ NLP.Dictionary.StarDict.Common: Resource :: [FilePath] -> DataEntry
+ NLP.Dictionary.StarDict.Common: UTF8Text :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: WAVEAudio :: ByteString -> DataEntry
+ NLP.Dictionary.StarDict.Common: WrongIfoFormat :: FilePath -> String -> StarDictException
+ NLP.Dictionary.StarDict.Common: WrongIndexFormat :: FilePath -> String -> StarDictException
+ NLP.Dictionary.StarDict.Common: XDXF :: Text -> DataEntry
+ NLP.Dictionary.StarDict.Common: [ifoAuthor] :: IfoFile -> Maybe Text
+ NLP.Dictionary.StarDict.Common: [ifoBookName] :: IfoFile -> Text
+ NLP.Dictionary.StarDict.Common: [ifoDate] :: IfoFile -> Maybe UTCTime
+ NLP.Dictionary.StarDict.Common: [ifoDescription] :: IfoFile -> Maybe Text
+ NLP.Dictionary.StarDict.Common: [ifoDictType] :: IfoFile -> Maybe String
+ NLP.Dictionary.StarDict.Common: [ifoEmail] :: IfoFile -> Maybe Text
+ NLP.Dictionary.StarDict.Common: [ifoIdxFileSize] :: IfoFile -> Int
+ NLP.Dictionary.StarDict.Common: [ifoIdxOffsetBits] :: IfoFile -> Maybe Int
+ NLP.Dictionary.StarDict.Common: [ifoMagicData] :: IfoFile -> ByteString
+ NLP.Dictionary.StarDict.Common: [ifoSameTypeSequence] :: IfoFile -> Maybe String
+ NLP.Dictionary.StarDict.Common: [ifoSynWordCount] :: IfoFile -> Maybe Int
+ NLP.Dictionary.StarDict.Common: [ifoVersion] :: IfoFile -> String
+ NLP.Dictionary.StarDict.Common: [ifoWebsite] :: IfoFile -> Maybe Text
+ NLP.Dictionary.StarDict.Common: [ifoWordCount] :: IfoFile -> Int
+ NLP.Dictionary.StarDict.Common: checkDataFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m FilePath
+ NLP.Dictionary.StarDict.Common: checkFiles :: IfoFilePath -> [FilePath] -> IO (Maybe FilePath)
+ NLP.Dictionary.StarDict.Common: checkGZFiles :: IfoFilePath -> [FilePath] -> [FilePath] -> IO (Maybe (Either FilePath FilePath))
+ NLP.Dictionary.StarDict.Common: class (Dictionary d) => StarDict d
+ NLP.Dictionary.StarDict.Common: data DataEntry
+ NLP.Dictionary.StarDict.Common: data IfoFile
+ NLP.Dictionary.StarDict.Common: data StarDictException
+ NLP.Dictionary.StarDict.Common: getIfoFile :: StarDict d => d -> IfoFile
+ NLP.Dictionary.StarDict.Common: getIndexNumber :: Maybe Int -> Get Int
+ NLP.Dictionary.StarDict.Common: ifoDateFormat :: String
+ NLP.Dictionary.StarDict.Common: instance Control.DeepSeq.NFData NLP.Dictionary.StarDict.Common.IfoFile
+ NLP.Dictionary.StarDict.Common: instance GHC.Classes.Eq NLP.Dictionary.StarDict.Common.DataEntry
+ NLP.Dictionary.StarDict.Common: instance GHC.Classes.Eq NLP.Dictionary.StarDict.Common.IfoFile
+ NLP.Dictionary.StarDict.Common: instance GHC.Classes.Eq NLP.Dictionary.StarDict.Common.StarDictException
+ NLP.Dictionary.StarDict.Common: instance GHC.Exception.Exception NLP.Dictionary.StarDict.Common.StarDictException
+ NLP.Dictionary.StarDict.Common: instance GHC.Generics.Generic NLP.Dictionary.StarDict.Common.IfoFile
+ NLP.Dictionary.StarDict.Common: instance GHC.Show.Show NLP.Dictionary.StarDict.Common.DataEntry
+ NLP.Dictionary.StarDict.Common: instance GHC.Show.Show NLP.Dictionary.StarDict.Common.IfoFile
+ NLP.Dictionary.StarDict.Common: instance GHC.Show.Show NLP.Dictionary.StarDict.Common.StarDictException
+ NLP.Dictionary.StarDict.Common: mkDataParser :: Maybe String -> Get [DataEntry]
+ NLP.Dictionary.StarDict.Common: mkDictionary :: (StarDict d, MonadThrow m, MonadIO m) => Tagged d IfoFilePath -> Renderer -> m d
+ NLP.Dictionary.StarDict.Common: putIndexNumber :: Maybe Int -> (Int -> Builder)
+ NLP.Dictionary.StarDict.Common: readIfoFile :: (MonadThrow m, MonadIO m) => FilePath -> m IfoFile
+ NLP.Dictionary.StarDict.Common: readIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> Get Int -> m [IndexEntry]
+ NLP.Dictionary.StarDict.Common: renderIfoFile :: IfoFile -> Text
+ NLP.Dictionary.StarDict.Common: renderIndexFile :: [IndexEntry] -> (Int -> Builder) -> ByteString
+ NLP.Dictionary.StarDict.Common: type IfoFilePath = FilePath
+ NLP.Dictionary.StarDict.Common: type IndexEntry = (Text, (Int, Int))
+ NLP.Dictionary.StarDict.Common: type Renderer = DataEntry -> Text
+ NLP.Dictionary.StarDict.InMemory: instance Control.DeepSeq.NFData NLP.Dictionary.StarDict.InMemory.Implementation
+ NLP.Dictionary.StarDict.InMemory: instance GHC.Generics.Generic NLP.Dictionary.StarDict.InMemory.Implementation
+ NLP.Dictionary.StarDict.InMemory: instance NLP.Dictionary.Dictionary NLP.Dictionary.StarDict.InMemory.Implementation
+ NLP.Dictionary.StarDict.InMemory: instance NLP.Dictionary.StarDict.Common.StarDict NLP.Dictionary.StarDict.InMemory.Implementation
+ NLP.Dictionary.StarDict.InMemory: tag :: IfoFilePath -> Tagged Implementation IfoFilePath
+ NLP.Dictionary.StarDict.Regular: instance Control.DeepSeq.NFData NLP.Dictionary.StarDict.Regular.Implementation
+ NLP.Dictionary.StarDict.Regular: instance GHC.Generics.Generic NLP.Dictionary.StarDict.Regular.Implementation
+ NLP.Dictionary.StarDict.Regular: instance NLP.Dictionary.Dictionary NLP.Dictionary.StarDict.Regular.Implementation
+ NLP.Dictionary.StarDict.Regular: instance NLP.Dictionary.StarDict.Common.StarDict NLP.Dictionary.StarDict.Regular.Implementation
+ NLP.Dictionary.StarDict.Regular: tag :: IfoFilePath -> Tagged Implementation IfoFilePath
- NLP.Dictionary.StarDict: mkDictionary :: (MonadThrow m, MonadIO m) => IfoFilePath -> Renderer -> m StarDict
+ NLP.Dictionary.StarDict: mkDictionary :: (StarDict d, MonadThrow m, MonadIO m) => Tagged d IfoFilePath -> Renderer -> m d
Files
- dictionaries.cabal +68/−1
- src/NLP/Dictionary/StarDict.hs +15/−333
- src/NLP/Dictionary/StarDict/Common.hs +371/−0
- src/NLP/Dictionary/StarDict/InMemory.hs +34/−25
- src/NLP/Dictionary/StarDict/Regular.hs +74/−0
- tests/Benchmark.hs +109/−0
- tests/Test.hs +34/−0
dictionaries.cabal view
@@ -1,5 +1,5 @@ name: dictionaries-version: 0.1.0.1+version: 0.2.0 synopsis: Tools to handle StarDict dictionaries. -- description: license: BSD3@@ -24,7 +24,9 @@ exposed-modules: NLP.Dictionary NLP.Dictionary.StarDict+ NLP.Dictionary.StarDict.Common NLP.Dictionary.StarDict.InMemory+ NLP.Dictionary.StarDict.Regular build-depends: base >= 4.8.2 && < 4.11 , attoparsec >= 0.10.4.0 && < 0.14@@ -32,12 +34,77 @@ , bytestring >= 0.10.6.0 && < 0.11 , containers >= 0.5.6.2 && < 0.6 , data-default >= 0.7.1.1 && < 0.8+ , deepseq , directory >= 1.2.2.0 && < 1.4 , exceptions >= 0.8.3 && < 0.9 , filepath >= 1.4.0.0 && < 1.5+ , tagged , text >= 1.2.2.1 && < 1.3 , time >= 1.5.0.1 && < 1.9 , transformers >= 0.4.2.0 && < 0.6 , zlib >= 0.6.1.2 && < 0.7 hs-source-dirs: src default-language: Haskell2010++ ghc-options: -Wall+ if flag(dev)+ ghc-options: -Werror+ else+ ghc-options: -O2+++test-suite tests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Test.hs++ build-depends: base >= 4.7 && < 5.0+ , QuickCheck >= 2.4 && < 3.0+ , bytestring+ , hspec >= 2.0 && < 3.0+ , tagged+ , time >= 1.5 && < 1.8+ , containers+ , dictionaries+ , directory+ , filepath+ , random+ , text+ default-language: Haskell2010++ ghc-options: -Wall+ if flag(dev)+ ghc-options: -Werror+ else+ ghc-options: -O2+++executable bench+ -- type: exitcode-stdio-1.0++ hs-source-dirs: tests+ main-is: Benchmark.hs++ default-language: Haskell2010++ build-depends: base >= 4.7 && < 5.0+ , bytestring+ , criterion >= 0.6.2.1 && < 1.2+ , containers+ , dictionaries+ , directory+ , exceptions+ , filepath+ , random+ , random-shuffle+ , tagged+ , text+ , transformers+ , deepseq++ ghc-options: -Wall+ if flag(dev)+ ghc-options: -Werror+ else+ ghc-options: -O2+
src/NLP/Dictionary/StarDict.hs view
@@ -7,343 +7,25 @@ = Description Tools for StarDict dictionaries.- To load a dictionary you should call 'mkDictionary' with path to .ifo file- and render function.- Every call of getEntry will perform file reading operation to retrieve- requested data. For in-memory version see 'NLP.Dictionary.StarDict.InMemory'.--}+ This module (re)exports core classes that are needed to work with+ dictionaries. To create dictionary, use `mkDictionary` method+ together with `tag` from one of implementations: + @+ import NLP.Dictionary.StarDict (StarDict(..))+ import qualified NLP.Dictionery.StarDict.Regular as SDR -{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}+ ... -module NLP.Dictionary.StarDict (- StarDict (..)- , StarDictException (..)- , mkDictionary+ dictionary <- mkDictionary (SDR.tag "/path/to/a/dictionary.ifo") renderer+ @+-} - , mkDataParser- , DataEntry (..)- , Renderer - , checkFiles- , checkGZFiles-- , IfoFile(..)- , IfoFilePath- , readIfoFile- , indexNumberParser- , ifoDateFormat-- , Index- , IndexEntry- , readIndexFile-- , checkDataFile+module NLP.Dictionary.StarDict (+ DataEntry(..)+ , Renderer+ , StarDict(..) ) where -import Prelude hiding (takeWhile)-import Control.Applicative (liftA2, many)-import Control.Arrow ((***))-import Control.Monad (when, unless)-import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Catch (Exception, MonadThrow, throwM)-import Data.Attoparsec.ByteString.Lazy (Result(..), Parser, parse, string, takeWhile, inClass)-import Data.Attoparsec.ByteString.Char8 (isEndOfLine, endOfLine, skipSpace, char)-import Data.Binary.Get (Get, runGet, runGetOrFail, isEmpty)-import Data.Binary.Get (getRemainingLazyByteString, getLazyByteStringNul, getLazyByteString)-import Data.Binary.Get (getWord32be, getWord64be)-import Data.ByteString.Lazy (ByteString)-import Data.Char (chr)-import Data.List (intercalate)-import Data.Map.Strict (Map)-import Data.Maybe (maybeToList)-import Data.Typeable (Typeable)-import Data.Time (parseTimeM, defaultTimeLocale)-import Data.Time.Clock (UTCTime)-import Data.Text.Lazy (Text)-import Data.Text.Lazy.Encoding (decodeUtf8, decodeLatin1)-import System.Directory (doesFileExist, getTemporaryDirectory)-import System.FilePath.Posix (dropExtension, joinPath, takeBaseName, (-<.>), (<.>))-import System.IO (Handle, IOMode(..), SeekMode(..), withFile, hSeek)-import NLP.Dictionary (Dictionary(..))-import qualified Codec.Compression.GZip as GZip-import qualified Data.ByteString.Lazy as BS-import qualified Data.ByteString.Lazy.Char8 as BSC8-import qualified Data.Map.Strict as Map-import qualified Data.Text.Lazy as T---- | Exceptions that are thrown when something with this module went wrong.-data StarDictException- = WrongIfoFormat FilePath String- -- ^ Thrown when information file (.ifo) has unsupported format.-- | IndexNotFound FilePath- -- ^ Thrown when index file (.idx, .idx.gz) is not found.-- | WrongIndexFormat FilePath String- -- ^ Thrown when index file has unsupported format.-- | DictionaryNotFound FilePath- -- ^ Thrown when dictionary file (.dict, .dict.dz) has unsupported format.- deriving (Eq, Show, Typeable)--instance Exception StarDictException----- | Representation of .ifo file.-data IfoFile = IfoFile {- ifoMagicData :: ByteString- , ifoVersion :: String- , ifoBookName :: Text- , ifoWordCount :: Int- , ifoIdxFileSize :: Int- , ifoIdxOffsetBits :: Maybe Int- , ifoSynWordCount :: Maybe Int- , ifoAuthor :: Maybe Text- , ifoEmail :: Maybe Text- , ifoWebsite :: Maybe Text- , ifoDescription :: Maybe Text- , ifoDate :: Maybe UTCTime- , ifoSameTypeSequence :: Maybe String- , ifoDictType :: Maybe String- } deriving (Eq, Show)---- | Date format of 'ifoDate' in IfoFile.-ifoDateFormat :: String-ifoDateFormat = "%0Y.%m.%d"---- | Read .ifo file at the given path.-readIfoFile :: (MonadThrow m, MonadIO m) => FilePath -> m IfoFile-readIfoFile ifoPath = (liftIO . BS.readFile $ ifoPath) >>= parseContents where- parseContents contents = case (parse ifoFile contents) of- (Fail _ _ msg) -> throwM $ WrongIfoFormat ifoPath msg- (Done _ r) -> return r-- expect :: (Eq a, Show a) => String -> a -> [a] -> Parser ()- expect name x xs = unless (x `elem` xs) . fail . concat $ [- name, " must be ", fmts xs, " (", show x, " provided)"- ] where- fmt y = '\'':(show y) ++ "'"-- fmts = \case- [] -> ""- (y:[]) -> fmt y- ys -> (intercalate ", " . map fmt . init $ ys) ++ " or " ++ (fmt . last $ ys)-- justExpect :: (Eq a, Show a) => String -> Maybe a -> [a] -> Parser ()- justExpect name mx xs = maybe (return ()) (\x -> expect name x xs) mx-- ifoFile :: Parser IfoFile- ifoFile = do- ifoMagicData <- magicData- expect "magic data" ifoMagicData ["StarDict's dict ifo file"]-- (_, ifoVersion) <- (id *** BSC8.unpack) <$> (endOfLine *> pair (Just "version"))- expect "version" ifoVersion ["2.4.2", "3.0.0"]-- ifoData <- Map.fromList <$> (endOfLine *> (many (pair Nothing) <* endOfLine))- let get = flip Map.lookup ifoData- let require field = ( $ (get field)) $ maybe- (fail $ "required field " ++ BSC8.unpack field ++ " not found") (return)-- ifoBookName <- decodeUtf8 <$> require "bookname"- ifoWordCount <- read . BSC8.unpack <$> require "wordcount"- ifoIdxFileSize <- read . BSC8.unpack <$> require "idxfilesize"-- let ifoIdxOffsetBits = read . BSC8.unpack <$> get "idxoffsetbits"- justExpect "idxoffsetbits" ifoIdxOffsetBits [32, 64]-- let ifoSynWordCount = read . BSC8.unpack <$> get "synwordcount"- let ifoAuthor = decodeUtf8 <$> get "author"- let ifoEmail = decodeUtf8 <$> get "email"- let ifoWebsite = decodeUtf8 <$> get "website"- let ifoDescription = decodeUtf8 <$> get "description"-- let ifoDate = get "date" >>= parseTimeM False defaultTimeLocale ifoDateFormat . BSC8.unpack-- let ifoSameTypeSequence = BSC8.unpack <$> get "sametypesequence"-- let ifoDictType = BSC8.unpack <$> get "dicttype"- justExpect "dicttype" ifoDictType ["wordnet"]-- return IfoFile {..}--- magicData :: Parser ByteString- magicData = BS.fromStrict <$> takeWhile (not . isEndOfLine)-- pair :: Maybe ByteString -> Parser (ByteString, ByteString)- pair = pair' . maybe- (takeWhile $ inClass "A-Za-z0-9-_")- (string . BS.toStrict) where-- pair' key = do- k <- BS.fromStrict <$> (skipSpace *> key)- _ <- skipSpace *> char '='- v <- BS.fromStrict <$> (skipSpace *> takeWhile (not . isEndOfLine))- return (k, v)---- | Get 32-bit or 64-bit integer depending on description in the .ifo file.-indexNumberParser :: IfoFile -> Get Int-indexNumberParser IfoFile {..} = case ifoIdxOffsetBits of- (Just 64) -> fromIntegral <$> getWord64be- _ -> fromIntegral <$> getWord32be---- | Representation of an .idx file.-type Index = Map Text (Int, Int)---- | Representation of an .idx file entry.-type IndexEntry = (Text, (Int, Int))---- | Given .ifo file name and list of extensions, returns first existing file with the same basename.-checkFiles :: IfoFilePath -> [FilePath] -> IO (Maybe FilePath)-checkFiles _ [] = return Nothing-checkFiles ifoPath (ext:exts) = let fn = ifoPath -<.> ext- in (doesFileExist fn) >>= \case- True -> return . Just $ fn- False -> checkFiles ifoPath exts---- | Given .ifo file name and two lists of extensions, returns first--- existing file with with the same basename and extension from the first--- list or (if such file doesn't exists) from the second list.-checkGZFiles- :: IfoFilePath- -> [FilePath]- -> [FilePath]- -> IO (Maybe (Either FilePath FilePath))-checkGZFiles ifoPath exts exts' = (checkFiles ifoPath exts) >>= maybe- (fmap (Right <$>) (checkFiles ifoPath exts'))- (return . Just . Left)---- | Type synonym to distinguish usage of paths.-type IfoFilePath = FilePath---- | Read .idx (.idx.gz) file.-readIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> Get Int -> m Index-readIndexFile fn num = checkIndexFile fn >>= getIndexContents >>= mkIndex where-- checkIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m (Either FilePath FilePath)- checkIndexFile ifoPath = (liftIO $ checkGZFiles ifoPath ["idx"] ["idx.gz"]) >>= \case- Nothing -> throwM $ IndexNotFound ifoPath- Just path -> return path-- getIndexContents :: (MonadThrow m, MonadIO m)- => Either FilePath FilePath -> m (FilePath, ByteString)- getIndexContents path = liftIO . fmap (fn,) . postprocess . BS.readFile $ fn where- postprocess = either (const id) (const $ fmap GZip.decompress) path- fn = either id id path-- mkIndex :: (MonadThrow m, MonadIO m) => (FilePath, ByteString) -> m Index- mkIndex (fn, contents) = either- (\(_, _, err) -> throwM $ WrongIndexFormat fn err)- (\(_, _, res) -> return . Map.fromList $ res)- (runGetOrFail getIndexEntries contents)-- getIndexEntries :: Get [IndexEntry]- getIndexEntries = isEmpty >>= \case- True -> return []- False -> liftA2 (:) getIndexEntry getIndexEntries-- getIndexEntry :: Get IndexEntry- getIndexEntry = (,) <$> (decodeUtf8 <$> getLazyByteStringNul)- <*> ((,) <$> num <*> num)---- | Returns path of decompressed dictionary.-checkDataFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m FilePath-checkDataFile ifoPath = (liftIO $ checkGZFiles ifoPath ["dict1"] ["dict.dz"]) >>= \case- Nothing -> throwM $ DictionaryNotFound ifoPath- Just (Left fn) -> return fn- Just (Right fn) -> liftIO $ do- fn' <- (joinPath . (:[(takeBaseName ifoPath) <.> "dict"])) <$> getTemporaryDirectory- GZip.decompress <$> (BS.readFile fn) >>= BS.writeFile fn'- return fn'---- | Possible dictionary entry formats.-data DataEntry- = UTF8Text Text- | LocaleText Text- | Pango Text- | Phonetics Text- | XDXF Text- | CJK Text- | PowerWord Text- | MediaWiki Text- | HTML Text- | Resource [FilePath]- | WAVEAudio ByteString- | Picture ByteString- | Reserved ByteString- deriving (Eq, Show)---- | Parser for a list of elements.-getMany :: Get a -> Get [a]-getMany p = isEmpty >>= \case- True -> return []- False -> liftA2 (:) p (getMany p)---- | Returns parser based on description in .ifo file.-mkDataParser :: Maybe String -> Get [DataEntry]-mkDataParser = maybe (getMany getGenericEntry) getSpecificEntries where-- getGenericEntry :: Get DataEntry- getGenericEntry = BSC8.head <$> getLazyByteString 1- >>= getSpecificEntry getLazyByteStringNul-- getSpecificEntries :: [Char] -> Get [DataEntry]- getSpecificEntries cs = sequence $ zipWith getSpecificEntry ps cs where- ps :: [Get ByteString]- ps = reverse . take (length cs) $ getRemainingLazyByteString:(repeat getLazyByteStringNul)-- getSpecificEntry :: Get ByteString -> Char -> Get DataEntry- getSpecificEntry getData = \case- 'm' -> UTF8Text . decodeUtf8 <$> getData- 'l' -> LocaleText . decodeLatin1 <$> getData- 'g' -> Pango . decodeUtf8 <$> getData- 't' -> Phonetics . decodeUtf8 <$> getData- 'x' -> XDXF . decodeUtf8 <$> getData- 'y' -> CJK . decodeUtf8 <$> getData- 'k' -> PowerWord . decodeUtf8 <$> getData- 'w' -> MediaWiki . decodeUtf8 <$> getData- 'h' -> HTML . decodeUtf8 <$> getData- 'n' -> Resource . lines . T.unpack . decodeUtf8 <$> getData- 'r' -> Resource . lines . T.unpack . decodeUtf8 <$> getData- 'W' -> WAVEAudio <$> getData- 'P' -> Picture <$> getData- 'X' -> Reserved <$> getData- _ -> error "type not supported"---- | Type of function to transform dictionary entries to a text.-type Renderer = DataEntry -> Text---- | Representation of the dictionary.-data StarDict = StarDict {- sdIfoFile :: IfoFile- , sdIndex :: Index- , sdDataPath :: FilePath- , sdDataParser :: Get [DataEntry]- , sdRender :: Renderer- }---- | Create dictionary.-mkDictionary :: (MonadThrow m, MonadIO m) => IfoFilePath -> Renderer -> m StarDict-mkDictionary ifoPath sdRender = do- sdIfoFile <- readIfoFile ifoPath- sdIndex <- readIndexFile ifoPath (indexNumberParser sdIfoFile)- sdDataPath <- checkDataFile ifoPath- let sdDataParser = mkDataParser (ifoSameTypeSequence sdIfoFile)- return StarDict {..}--instance Dictionary StarDict where- getEntries str (StarDict {..}) = withFile sdDataPath ReadMode extractEntries where-- extractEntries :: Handle -> IO [Text]- extractEntries h = mapM (extractEntry h) . maybeToList . Map.lookup str $ sdIndex-- extractEntry :: Handle -> (Int, Int) -> IO Text- extractEntry h (offset, size) = do- hSeek h AbsoluteSeek (fromIntegral offset)- T.concat . map sdRender . runGet sdDataParser <$> BS.hGet h size-+import NLP.Dictionary.StarDict.Common (StarDict(..), DataEntry(..), Renderer)
+ src/NLP/Dictionary/StarDict/Common.hs view
@@ -0,0 +1,371 @@+{-|+ Module: NLP.Dictionary.StarDict.Common+ Copyright: (c) 2016 Al Zohali+ License: BSD3+ Maintainer: Al Zohali <zohl@fmap.me>+ Stability: experimental++ = Description+ Common functions and types for StarDict dictionaries.+-}+++{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE DeriveGeneric #-}++module NLP.Dictionary.StarDict.Common (+ StarDictException (..)++ , checkFiles+ , checkGZFiles++ , IfoFile(..)+ , IfoFilePath+ , ifoDateFormat+ , readIfoFile+ , renderIfoFile++ , IndexEntry+ , readIndexFile+ , renderIndexFile+ , getIndexNumber+ , putIndexNumber++ , checkDataFile++ , DataEntry (..)+ , Renderer+ , mkDataParser++ , StarDict (..)+ ) where++import Prelude hiding (takeWhile)+import Control.Applicative (liftA2, many)+import Control.Arrow ((***))+import Control.Monad (unless)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Catch (Exception, MonadThrow, throwM)+import Data.Attoparsec.ByteString.Lazy (Result(..), Parser, parse, string, takeWhile, inClass)+import Data.Attoparsec.ByteString.Char8 (isEndOfLine, endOfLine, skipSpace, char)+import Data.Binary.Get (Get, runGetOrFail, isEmpty)+import Data.Binary.Get (getRemainingLazyByteString, getLazyByteStringNul, getLazyByteString)+import Data.Binary.Get (getWord32be, getWord64be)+import Data.Binary.Builder (Builder, fromLazyByteString, empty, toLazyByteString)+import Data.Binary.Builder (putWord64be, putWord32be, singleton)+import Data.ByteString.Lazy (ByteString)+import Data.List (intercalate)+import Data.Maybe (catMaybes)+import Data.Monoid ((<>))+import Data.Tagged (Tagged)+import Data.Typeable (Typeable)+import Data.Time (parseTimeM, defaultTimeLocale, formatTime)+import Data.Time.Clock (UTCTime)+import Data.Text.Lazy (Text)+import Data.Text.Lazy.Encoding (decodeUtf8, encodeUtf8, decodeLatin1)+import NLP.Dictionary (Dictionary)+import System.Directory (doesFileExist, getTemporaryDirectory)+import System.FilePath.Posix (joinPath, takeBaseName, (-<.>), (<.>))+import qualified Codec.Compression.GZip as GZip+import qualified Data.ByteString.Lazy as BS+import qualified Data.ByteString.Lazy.Char8 as BSC8+import qualified Data.Map.Strict as Map+import qualified Data.Text.Lazy as T+import Control.DeepSeq (NFData(..))+import GHC.Generics (Generic)+++-- | Exceptions that are thrown when something with this module went wrong.+data StarDictException+ = WrongIfoFormat FilePath String+ -- ^ Thrown when information file (.ifo) has unsupported format.++ | IndexNotFound FilePath+ -- ^ Thrown when index file (.idx, .idx.gz) is not found.++ | WrongIndexFormat FilePath String+ -- ^ Thrown when index file has unsupported format.++ | DictionaryNotFound FilePath+ -- ^ Thrown when dictionary file (.dict, .dict.dz) has unsupported format.+ deriving (Eq, Show, Typeable)++instance Exception StarDictException+++-- | Type synonym to distinguish usage of paths.+type IfoFilePath = FilePath++-- | Representation of .ifo file.+data IfoFile = IfoFile {+ ifoMagicData :: ByteString -- ^ Corresponds to the first string in the file.+ , ifoVersion :: String -- ^ Corresponds to version field.+ , ifoBookName :: Text -- ^ Corresponds to bookname field.+ , ifoWordCount :: Int -- ^ Corresponds to wordcount field.+ , ifoIdxFileSize :: Int -- ^ Corresponds to idxfilesize field.+ , ifoIdxOffsetBits :: Maybe Int -- ^ Corresponds to idxoffsetbits field.+ , ifoSynWordCount :: Maybe Int -- ^ Corresponds to synwordcount field.+ , ifoAuthor :: Maybe Text -- ^ Corresponds to author field.+ , ifoEmail :: Maybe Text -- ^ Corresponds to email field.+ , ifoWebsite :: Maybe Text -- ^ Corresponds to website field.+ , ifoDescription :: Maybe Text -- ^ Corresponds to description field.+ , ifoDate :: Maybe UTCTime -- ^ Corresponds to date field.+ , ifoSameTypeSequence :: Maybe String -- ^ Corresponds to sametypesequence field.+ , ifoDictType :: Maybe String+ } deriving (Eq, Show, Generic)++instance NFData IfoFile++-- | Date format of 'ifoDate' in IfoFile.+ifoDateFormat :: String+ifoDateFormat = "%0Y.%m.%d"++-- | Read .ifo file at the given path.+readIfoFile :: (MonadThrow m, MonadIO m) => FilePath -> m IfoFile+readIfoFile ifoPath = (liftIO . BS.readFile $ ifoPath) >>= parseContents where+ parseContents contents = case (parse ifoFile contents) of+ (Fail _ _ msg) -> throwM $ WrongIfoFormat ifoPath msg+ (Done _ r) -> return r++ expect :: (Eq a, Show a) => String -> a -> [a] -> Parser ()+ expect name x xs = unless (x `elem` xs) . fail . concat $ [+ name, " must be ", fmts xs, " (", show x, " provided)"+ ] where+ fmt y = '\'':(show y) ++ "'"++ fmts = \case+ [] -> ""+ (y:[]) -> fmt y+ ys -> (intercalate ", " . map fmt . init $ ys) ++ " or " ++ (fmt . last $ ys)++ justExpect :: (Eq a, Show a) => String -> Maybe a -> [a] -> Parser ()+ justExpect name mx xs = maybe (return ()) (\x -> expect name x xs) mx++ ifoFile :: Parser IfoFile+ ifoFile = do+ ifoMagicData <- magicData+ expect "magic data" ifoMagicData ["StarDict's dict ifo file"]++ (_, ifoVersion) <- (id *** BSC8.unpack) <$> (endOfLine *> pair (Just "version"))+ expect "version" ifoVersion ["2.4.2", "3.0.0"]++ ifoData <- Map.fromList <$> (endOfLine *> (many (pair Nothing) <* endOfLine))+ let get = flip Map.lookup ifoData+ let require field = ( $ (get field)) $ maybe+ (fail $ "required field " ++ BSC8.unpack field ++ " not found") (return)++ ifoBookName <- decodeUtf8 <$> require "bookname"+ ifoWordCount <- read . BSC8.unpack <$> require "wordcount"+ ifoIdxFileSize <- read . BSC8.unpack <$> require "idxfilesize"++ let ifoIdxOffsetBits = read . BSC8.unpack <$> get "idxoffsetbits"+ justExpect "idxoffsetbits" ifoIdxOffsetBits [32, 64]++ let ifoSynWordCount = read . BSC8.unpack <$> get "synwordcount"+ let ifoAuthor = decodeUtf8 <$> get "author"+ let ifoEmail = decodeUtf8 <$> get "email"+ let ifoWebsite = decodeUtf8 <$> get "website"+ let ifoDescription = decodeUtf8 <$> get "description"++ let ifoDate = get "date" >>= parseTimeM False defaultTimeLocale ifoDateFormat . BSC8.unpack++ let ifoSameTypeSequence = BSC8.unpack <$> get "sametypesequence"++ let ifoDictType = BSC8.unpack <$> get "dicttype"+ justExpect "dicttype" ifoDictType ["wordnet"]++ return IfoFile {..}+++ magicData :: Parser ByteString+ magicData = BS.fromStrict <$> takeWhile (not . isEndOfLine)++ pair :: Maybe ByteString -> Parser (ByteString, ByteString)+ pair = pair' . maybe+ (takeWhile $ inClass "A-Za-z0-9-_")+ (string . BS.toStrict) where++ pair' key = do+ k <- BS.fromStrict <$> (skipSpace *> key)+ _ <- skipSpace *> char '='+ v <- BS.fromStrict <$> (skipSpace *> takeWhile (not . isEndOfLine))+ return (k, v)++-- | Generates .ifo file contents based on 'IfoFile'+renderIfoFile :: IfoFile -> Text+renderIfoFile IfoFile {..} = T.intercalate "\n" $ [+ decodeUtf8 ifoMagicData+ , "version=" <> (T.pack ifoVersion)+ , "bookname=" <> ifoBookName+ , "wordcount=" <> (T.pack . show $ ifoWordCount)+ , "idxfilesize=" <> (T.pack . show $ ifoIdxFileSize)+ ] ++ catMaybes [+ (("idxoffsetbits=" <>) . T.pack . show) <$> ifoIdxOffsetBits+ , (("synwordcount=" <>) . T.pack . show) <$> ifoSynWordCount+ , ("author=" <>) <$> ifoAuthor+ , ("email=" <>) <$> ifoEmail+ , ("website=" <>) <$> ifoWebsite+ , ("description=" <>) <$> ifoDescription+ , (("date=" <>) . T.pack+ . formatTime defaultTimeLocale ifoDateFormat) <$> ifoDate+ , (("sametypesequence=" <>) . T.pack) <$> ifoSameTypeSequence+ , (("dicttype=" <>) . T.pack) <$> ifoDictType+ ]++-- | Get 32-bit or 64-bit integer depending on description in the .ifo file.+getIndexNumber :: Maybe Int -> Get Int+getIndexNumber = \case+ (Just 64) -> fromIntegral <$> getWord64be+ _ -> fromIntegral <$> getWord32be++-- | Put 32-bit or 64-bit integer depending on description in the .ifo file.+putIndexNumber :: Maybe Int -> (Int -> Builder)+putIndexNumber = \case+ (Just 64) -> putWord64be . fromIntegral+ _ -> putWord32be . fromIntegral++-- | Representation of an .idx file entry.+type IndexEntry = (Text, (Int, Int))++-- | Given .ifo file name and list of extensions, returns first existing file with the same basename.+checkFiles :: IfoFilePath -> [FilePath] -> IO (Maybe FilePath)+checkFiles _ [] = return Nothing+checkFiles ifoPath (ext:exts) = let fn = ifoPath -<.> ext+ in (doesFileExist fn) >>= \case+ True -> return . Just $ fn+ False -> checkFiles ifoPath exts++-- | Given .ifo file name and two lists of extensions, returns first+-- existing file with with the same basename and extension from the first+-- list or (if such file doesn't exists) from the second list.+checkGZFiles+ :: IfoFilePath+ -> [FilePath]+ -> [FilePath]+ -> IO (Maybe (Either FilePath FilePath))+checkGZFiles ifoPath exts exts' = (checkFiles ifoPath exts) >>= maybe+ (fmap (Right <$>) (checkFiles ifoPath exts'))+ (return . Just . Left)+++-- | Read .idx (.idx.gz) file.+readIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> Get Int -> m [IndexEntry]+readIndexFile fn num = checkIndexFile fn >>= getIndexContents >>= mkIndex where++ checkIndexFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m (Either FilePath FilePath)+ checkIndexFile ifoPath = (liftIO $ checkGZFiles ifoPath ["idx"] ["idx.gz"]) >>= \case+ Nothing -> throwM $ IndexNotFound ifoPath+ Just path -> return path++ getIndexContents :: (MonadIO m)+ => Either FilePath FilePath -> m (FilePath, ByteString)+ getIndexContents path = liftIO . fmap (fn',) . postprocess . BS.readFile $ fn' where+ postprocess = either (const id) (const $ fmap GZip.decompress) path+ fn' = either id id path++ mkIndex :: (MonadThrow m) => (FilePath, ByteString) -> m [IndexEntry]+ mkIndex (fn', contents) = either+ (\(_, _, err) -> throwM $ WrongIndexFormat fn' err)+ (\(_, _, res) -> return $ res)+ (runGetOrFail getIndexEntries contents)++ getIndexEntries :: Get [IndexEntry]+ getIndexEntries = isEmpty >>= \case+ True -> return []+ False -> liftA2 (:) getIndexEntry getIndexEntries++ getIndexEntry :: Get IndexEntry+ getIndexEntry = (,) <$> (decodeUtf8 <$> getLazyByteStringNul)+ <*> ((,) <$> num <*> num)++-- | Generates .idx file contents based on 'Index'.+renderIndexFile :: [IndexEntry] -> (Int -> Builder) -> ByteString+renderIndexFile entries putNum = toLazyByteString $ buildIndex entries where++ buildIndex [] = empty+ buildIndex (e:es) = putEntry e <> buildIndex es++ putEntry (entry, (offset, size)) = foldr1 (<>) [+ (fromLazyByteString . encodeUtf8 $ entry)+ , singleton 0+ , putNum offset+ , putNum size+ ]+++-- | Returns path of decompressed dictionary.+checkDataFile :: (MonadThrow m, MonadIO m) => IfoFilePath -> m FilePath+checkDataFile ifoPath = (liftIO $ checkGZFiles ifoPath ["dict"] ["dict.dz"]) >>= \case+ Nothing -> throwM $ DictionaryNotFound ifoPath+ Just (Left fn) -> return fn+ Just (Right fn) -> liftIO $ do+ fn' <- (joinPath . (:[(takeBaseName ifoPath) <.> "dict"])) <$> getTemporaryDirectory+ GZip.decompress <$> (BS.readFile fn) >>= BS.writeFile fn'+ return fn'+++-- | Possible dictionary entry formats.+data DataEntry+ = UTF8Text Text+ | LocaleText Text+ | Pango Text+ | Phonetics Text+ | XDXF Text+ | CJK Text+ | PowerWord Text+ | MediaWiki Text+ | HTML Text+ | Resource [FilePath]+ | WAVEAudio ByteString+ | Picture ByteString+ | Reserved ByteString+ deriving (Eq, Show)++-- | Parser for a list of elements.+getMany :: Get a -> Get [a]+getMany p = isEmpty >>= \case+ True -> return []+ False -> liftA2 (:) p (getMany p)++-- | Returns parser based on description in .ifo file.+mkDataParser :: Maybe String -> Get [DataEntry]+mkDataParser = maybe (getMany getGenericEntry) getSpecificEntries where++ getGenericEntry :: Get DataEntry+ getGenericEntry = BSC8.head <$> getLazyByteString 1+ >>= getSpecificEntry getLazyByteStringNul++ getSpecificEntries :: [Char] -> Get [DataEntry]+ getSpecificEntries cs = sequence $ zipWith getSpecificEntry ps cs where+ ps :: [Get ByteString]+ ps = reverse . take (length cs) $ getRemainingLazyByteString:(repeat getLazyByteStringNul)++ getSpecificEntry :: Get ByteString -> Char -> Get DataEntry+ getSpecificEntry getData = \case+ 'm' -> UTF8Text . decodeUtf8 <$> getData+ 'l' -> LocaleText . decodeLatin1 <$> getData+ 'g' -> Pango . decodeUtf8 <$> getData+ 't' -> Phonetics . decodeUtf8 <$> getData+ 'x' -> XDXF . decodeUtf8 <$> getData+ 'y' -> CJK . decodeUtf8 <$> getData+ 'k' -> PowerWord . decodeUtf8 <$> getData+ 'w' -> MediaWiki . decodeUtf8 <$> getData+ 'h' -> HTML . decodeUtf8 <$> getData+ 'n' -> Resource . lines . T.unpack . decodeUtf8 <$> getData+ 'r' -> Resource . lines . T.unpack . decodeUtf8 <$> getData+ 'W' -> WAVEAudio <$> getData+ 'P' -> Picture <$> getData+ 'X' -> Reserved <$> getData+ _ -> error "type not supported"++-- | Type of function to transform dictionary entries to a text.+type Renderer = DataEntry -> Text+++-- | Classtype for stardict dictionaries.+class (Dictionary d) => StarDict d where+ getIfoFile :: d -> IfoFile+ mkDictionary :: (MonadThrow m, MonadIO m) => Tagged d IfoFilePath -> Renderer -> m d
src/NLP/Dictionary/StarDict/InMemory.hs view
@@ -6,50 +6,45 @@ Stability: experimental = Description- In-memory version of 'NLP.Dictionary.StarDict'.+ Implementation of an in-memory dictionary.+ All the entries will be loaded in the beginning into RAM, thus+ allowing faster access for the next queries. -} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-} -module NLP.Dictionary.StarDict.InMemory (- StarDict (..)- , mkDictionary- ) where+module NLP.Dictionary.StarDict.InMemory (tag) where -import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Catch (MonadThrow)-import Data.Binary.Get (Get, getWord32be)+import Control.DeepSeq (NFData)+import Control.Monad.IO.Class (liftIO) import Data.ByteString.Lazy (ByteString)+import Data.Map.Strict (Map) import Data.Maybe (maybeToList)+import Data.Tagged (Tagged(..), untag) import Data.Text.Lazy (Text) import Data.Text.Lazy.Encoding (decodeUtf8)+import GHC.Generics (Generic) import NLP.Dictionary (Dictionary(..))-import NLP.Dictionary.StarDict (IfoFile(..), IfoFilePath, readIfoFile, indexNumberParser)-import NLP.Dictionary.StarDict (Index, readIndexFile, checkDataFile, DataEntry(..), Renderer)-import NLP.Dictionary.StarDict (mkDataParser)+import NLP.Dictionary.StarDict.Common (IfoFile(..), IfoFilePath, readIfoFile, getIndexNumber, StarDict(..))+import NLP.Dictionary.StarDict.Common (readIndexFile, checkDataFile, Renderer) import qualified Data.ByteString.Lazy as BS import qualified Data.Map.Strict as Map --- | Representation of dictionary.-data StarDict = StarDict {++type Index = Map Text (Int, Int)++data Implementation = Implementation { sdIfoFile :: IfoFile , sdIndex :: Index , sdData :: ByteString- , sdDataParser :: Get [DataEntry] , sdRender :: Renderer- }+ } deriving Generic --- | Create dictionary.-mkDictionary :: (MonadThrow m, MonadIO m) => IfoFilePath -> Renderer -> m StarDict-mkDictionary ifoPath sdRender = do- sdIfoFile <- readIfoFile ifoPath- sdIndex <- readIndexFile ifoPath (indexNumberParser sdIfoFile)- sdData <- checkDataFile ifoPath >>= liftIO . BS.readFile- let sdDataParser = mkDataParser (ifoSameTypeSequence sdIfoFile)- return StarDict {..}+instance NFData Implementation -instance Dictionary StarDict where- getEntries str (StarDict {..}) = return extractEntries where+instance Dictionary Implementation where+ getEntries str (Implementation {..}) = return extractEntries where extractEntries :: [Text] extractEntries = map extractEntry . maybeToList . Map.lookup str $ sdIndex@@ -58,3 +53,17 @@ extractEntry (offset, size) = decodeUtf8 . BS.take (fromIntegral size) . BS.drop (fromIntegral offset) $ sdData++instance StarDict Implementation where+ getIfoFile = sdIfoFile++ mkDictionary taggedIfoPath sdRender = do+ let ifoPath = untag taggedIfoPath+ sdIfoFile <- readIfoFile ifoPath+ sdIndex <- Map.fromList <$> readIndexFile ifoPath (getIndexNumber . ifoIdxOffsetBits $ sdIfoFile)+ sdData <- checkDataFile ifoPath >>= liftIO . BS.readFile+ return Implementation {..}++-- | Tag for ifoPath to distinct dictionary type.+tag :: IfoFilePath -> Tagged Implementation IfoFilePath+tag = Tagged
+ src/NLP/Dictionary/StarDict/Regular.hs view
@@ -0,0 +1,74 @@+{-|+ Module: NLP.Dictionary.StarDict.Regular+ Copyright: (c) 2016 Al Zohali+ License: BSD3+ Maintainer: Al Zohali <zohl@fmap.me>+ Stability: experimental++ = Description+ Tools for StarDict dictionaries.+ Every call of 'getEntries' will perform file reading operation to+ retrieve requested data. For faster access see+ 'NLP.Dictionary.StarDict.InMemory'.+-}+++{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE DeriveGeneric #-}++module NLP.Dictionary.StarDict.Regular (tag) where++import Prelude hiding (takeWhile)+import Data.Binary.Get (runGet)+import Data.Map.Strict (Map)+import Data.Maybe (maybeToList)+import Data.Text.Lazy (Text)+import System.IO (Handle, IOMode(..), SeekMode(..), withFile, hSeek)+import NLP.Dictionary (Dictionary(..))+import qualified Data.ByteString.Lazy as BS+import qualified Data.Map.Strict as Map+import qualified Data.Text.Lazy as T+import Control.DeepSeq (NFData(..))+import GHC.Generics (Generic)+import NLP.Dictionary.StarDict.Common (StarDict(..), IfoFile(..), readIfoFile, readIndexFile, getIndexNumber, checkDataFile, mkDataParser, Renderer, IfoFilePath)+import Data.Tagged (Tagged(..), untag)+++type Index = Map Text (Int, Int)++data Implementation = Implementation {+ sdIfoFile :: IfoFile+ , sdIndex :: Index+ , sdDataPath :: FilePath+ , sdRender :: Renderer+ } deriving Generic++instance NFData Implementation++instance Dictionary Implementation where+ getEntries str (Implementation {..}) = withFile sdDataPath ReadMode extractEntries where++ extractEntries :: Handle -> IO [Text]+ extractEntries h = mapM (extractEntry h) . maybeToList . Map.lookup str $ sdIndex++ extractEntry :: Handle -> (Int, Int) -> IO Text+ extractEntry h (offset, size) = do+ hSeek h AbsoluteSeek (fromIntegral offset)+ T.concat . map sdRender . runGet (mkDataParser . ifoSameTypeSequence $ sdIfoFile) <$> BS.hGet h size++instance StarDict Implementation where+ getIfoFile = sdIfoFile++ mkDictionary taggedIfoPath sdRender = do+ let ifoPath = untag taggedIfoPath+ sdIfoFile <- readIfoFile ifoPath+ sdIndex <- Map.fromList <$> readIndexFile ifoPath (getIndexNumber . ifoIdxOffsetBits $ sdIfoFile)+ sdDataPath <- checkDataFile ifoPath+ return Implementation {..}++-- | Tag for ifoPath to distinct dictionary type.+tag :: IfoFilePath -> Tagged Implementation IfoFilePath+tag = Tagged
+ tests/Benchmark.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE BangPatterns #-}++import Control.DeepSeq (NFData(..))+import Control.Monad.Catch (MonadThrow)+import Control.Monad.IO.Class (MonadIO)+import Criterion (Benchmark, bench, nfIO, env)+import Criterion.Main (bgroup, defaultMain)+import Data.List (intercalate)+import NLP.Dictionary (Dictionary(..))+import NLP.Dictionary.StarDict (StarDict(..))+import NLP.Dictionary.StarDict.Common (IfoFilePath)+import System.Random (getStdGen)+import System.Random.Shuffle (shuffle')+import Utils (generateDictionary, generateStarDict, renderId)+import qualified NLP.Dictionary.StarDict.Regular as SDR+import qualified NLP.Dictionary.StarDict.InMemory as SDIM+++data DictionaryWrapper = forall d. (NFData d, Dictionary d) => WrapDictionary d++wrapDictionary :: (NFData d, Dictionary d) => d -> DictionaryWrapper+wrapDictionary = WrapDictionary++instance Dictionary DictionaryWrapper where+ getEntries name (WrapDictionary d) = getEntries name d++instance NFData DictionaryWrapper where+ rnf (WrapDictionary !_) = ()+++data DictionaryType+ = Regular+ | InMemory+ deriving (Eq, Show, Enum, Bounded)++mkWrappedDictionary :: (MonadIO m, MonadThrow m)+ => DictionaryType+ -> IfoFilePath+ -> m DictionaryWrapper+mkWrappedDictionary Regular = \p -> wrapDictionary <$> mkDictionary (SDR.tag p) renderId+mkWrappedDictionary InMemory = \p -> wrapDictionary <$> mkDictionary (SDIM.tag p) renderId+++around :: Int -> (Int, Int)+around x = let dx = min (x `div` 10) 1 in (x - dx, x + dx)++mkName :: Int -> Int -> Int -> String+mkName dictionarySize textSize wordSize = intercalate "_" $ [+ show dictionarySize+ , show textSize+ , show wordSize+ ]+++benchLoading :: Int -> Int -> Int -> DictionaryType -> Benchmark+benchLoading dictionarySize textSize wordSize dictionaryType = env+ (generateDictionary+ dictionarySize+ (around textSize)+ (around wordSize)+ >>= generateStarDict)+ $ \starDictPath -> bench (mkName dictionarySize textSize wordSize) $ do+ nfIO $ (mkWrappedDictionary dictionaryType starDictPath)+++benchAccessing :: Int -> Int -> Int -> DictionaryType -> Benchmark+benchAccessing dictionarySize textSize wordSize dictionaryType = env+ (do+ dict <- generateDictionary+ dictionarySize+ (around textSize)+ (around wordSize)++ sampleWords <- fmap (take 10)+ . fmap (shuffle' (map fst dict) (length dict))+ $ getStdGen++ starDict <- generateStarDict dict >>= mkWrappedDictionary dictionaryType++ return (starDict, sampleWords))++ $ \ ~(starDict, sampleWords) -> bench (mkName dictionarySize textSize wordSize) $ do+ nfIO $ mapM (flip getEntries starDict) sampleWords+++benchDictionaries :: ([DictionaryType -> Benchmark]) -> [Benchmark]+benchDictionaries bs = map+ (\dt -> bgroup (show dt) (map ($ dt) bs))+ [minBound..maxBound]+++main :: IO ()+main = defaultMain [+ bgroup "Loading" . benchDictionaries $ [+ benchLoading 100 100 100+ , benchLoading 500 100 100+ , benchLoading 100 500 100+ , benchLoading 100 100 500+ ]+ , bgroup "Accessing" . benchDictionaries $ [+ benchAccessing 100 100 100+ , benchAccessing 500 100 100+ , benchAccessing 100 500 100+ , benchAccessing 100 100 500+ ]+ ]+
+ tests/Test.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++import NLP.Dictionary (getEntries)+import NLP.Dictionary.StarDict (StarDict(..))+import Test.Hspec (Spec, hspec, describe, it, context, shouldBe)+import Utils (generateDictionary, generateStarDict, renderId)+import qualified NLP.Dictionary.StarDict.Regular as SDR+++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "RandomDictionary" randomDictionary++randomDictionary :: Spec+randomDictionary = do++ let generateDictionaries = do+ dictionary <- generateDictionary 10 (5, 15) (1, 10)+ starDictPath <- generateStarDict dictionary+ starDict <- mkDictionary (SDR.tag starDictPath) renderId+ return (dictionary, starDict)++ context "when getting entries" $ do+ it "matches every entry in the dictionary" $ do+ (dictionary, starDict) <- generateDictionaries+ mapM_ (\(name, entry) -> shouldBe [entry] =<< getEntries name starDict) dictionary++ it "doesn't match missing entry" $ do+ (_, starDict) <- generateDictionaries+ shouldBe [] =<< getEntries "# not found" starDict+