{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Numeric.Optimization.MIP
-- Copyright   :  (c) Masahiro Sakai 2011-2014
-- License     :  BSD-style
--
-- Maintainer  :  masahiro.sakai@gmail.com
-- Stability   :  provisional
-- Portability :  non-portable
--
-- Mixed-Integer Programming Problems with some commmonly used extensions
--
-----------------------------------------------------------------------------
module Numeric.Optimization.MIP
  (
  -- * The MIP Problem/Solution types
    module Numeric.Optimization.MIP.Base

  -- * File I/O
  -- $IO

  -- ** Reading problem files
  , readFile
  , readLPFile
  , readMPSFile
  , parseLPString
  , parseMPSString
  , ParseError

  -- ** Generating problem files
  , writeFile
  , writeLPFile
  , writeMPSFile
  , toLPString
  , toMPSString
  ) where

import Prelude hiding (readFile, writeFile)
import Control.Exception
import Data.Char
import Data.Scientific (Scientific)
import Data.String
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TLIO
import System.FilePath (takeExtension, splitExtension)
import System.IO hiding (readFile, writeFile)
import Text.Megaparsec (Stream (..))

import Numeric.Optimization.MIP.Base
import Numeric.Optimization.MIP.FileUtils (ParseError)
import qualified Numeric.Optimization.MIP.LPFile as LPFile
import qualified Numeric.Optimization.MIP.MPSFile as MPSFile

#ifdef WITH_ZLIB
import qualified Codec.Compression.GZip as GZip
import qualified Data.ByteString.Lazy as BL
import Data.ByteString.Lazy.Encoding (encode, decode)
import qualified Data.CaseInsensitive as CI
import GHC.IO.Encoding (getLocaleEncoding)
#endif

-- | Parse LP or MPS file based on file extension.
readFile :: FileOptions -> FilePath -> IO (Problem Scientific)
readFile :: FileOptions -> FilePath -> IO (Problem Scientific)
readFile FileOptions
opt FilePath
fname =
  case FilePath -> FilePath
getExt FilePath
fname of
    FilePath
".lp"  -> FileOptions -> FilePath -> IO (Problem Scientific)
readLPFile FileOptions
opt FilePath
fname
    FilePath
".mps" -> FileOptions -> FilePath -> IO (Problem Scientific)
readMPSFile FileOptions
opt FilePath
fname
    FilePath
ext -> IOError -> IO (Problem Scientific)
forall a. IOError -> IO a
ioError (IOError -> IO (Problem Scientific))
-> IOError -> IO (Problem Scientific)
forall a b. (a -> b) -> a -> b
$ FilePath -> IOError
userError (FilePath -> IOError) -> FilePath -> IOError
forall a b. (a -> b) -> a -> b
$ FilePath
"unknown extension: " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
ext

-- | Parse a file containing LP file data.
readLPFile :: FileOptions -> FilePath -> IO (Problem Scientific)
#ifndef WITH_ZLIB
readLPFile = LPFile.parseFile
#else
readLPFile :: FileOptions -> FilePath -> IO (Problem Scientific)
readLPFile FileOptions
opt FilePath
fname = do
  Text
s <- FileOptions -> FilePath -> IO Text
readTextFile FileOptions
opt FilePath
fname
  let ret :: Either (ParseError Text) (Problem Scientific)
ret = FileOptions
-> FilePath
-> Text
-> Either (ParseError Text) (Problem Scientific)
forall s.
(Stream s, Token s ~ Char, IsString (Tokens s)) =>
FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
LPFile.parseString FileOptions
opt FilePath
fname Text
s
  case Either (ParseError Text) (Problem Scientific)
ret of
    Left ParseError Text
e -> ParseError Text -> IO (Problem Scientific)
forall a e. Exception e => e -> a
throw ParseError Text
e
    Right Problem Scientific
a -> Problem Scientific -> IO (Problem Scientific)
forall (m :: * -> *) a. Monad m => a -> m a
return Problem Scientific
a
#endif

-- | Parse a file containing MPS file data.
readMPSFile :: FileOptions -> FilePath -> IO (Problem Scientific)
#ifndef WITH_ZLIB
readMPSFile = MPSFile.parseFile
#else
readMPSFile :: FileOptions -> FilePath -> IO (Problem Scientific)
readMPSFile FileOptions
opt FilePath
fname = do
  Text
s <- FileOptions -> FilePath -> IO Text
readTextFile FileOptions
opt FilePath
fname
  let ret :: Either (ParseError Text) (Problem Scientific)
ret = FileOptions
-> FilePath
-> Text
-> Either (ParseError Text) (Problem Scientific)
forall s.
(Stream s, Token s ~ Char, IsString (Tokens s)) =>
FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
MPSFile.parseString FileOptions
opt FilePath
fname Text
s
  case Either (ParseError Text) (Problem Scientific)
ret of
    Left ParseError Text
e -> ParseError Text -> IO (Problem Scientific)
forall a e. Exception e => e -> a
throw ParseError Text
e
    Right Problem Scientific
a -> Problem Scientific -> IO (Problem Scientific)
forall (m :: * -> *) a. Monad m => a -> m a
return Problem Scientific
a
#endif

readTextFile :: FileOptions -> FilePath -> IO TL.Text
#ifndef WITH_ZLIB
readTextFile opt fname = do
  h <- openFile fname ReadMode
  case optFileEncoding opt of
    Nothing -> return ()
    Just enc -> hSetEncoding h enc
  TLIO.hGetContents h
#else
readTextFile :: FileOptions -> FilePath -> IO Text
readTextFile FileOptions
opt FilePath
fname = do
  TextEncoding
enc <- case FileOptions -> Maybe TextEncoding
optFileEncoding FileOptions
opt of
         Maybe TextEncoding
Nothing -> IO TextEncoding
getLocaleEncoding
         Just TextEncoding
enc -> TextEncoding -> IO TextEncoding
forall (m :: * -> *) a. Monad m => a -> m a
return TextEncoding
enc
  let f :: ByteString -> ByteString
f = if FilePath -> CI FilePath
forall s. FoldCase s => s -> CI s
CI.mk (FilePath -> FilePath
takeExtension FilePath
fname) CI FilePath -> CI FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== CI FilePath
".gz" then ByteString -> ByteString
GZip.decompress else ByteString -> ByteString
forall a. a -> a
id
  ByteString
s <- FilePath -> IO ByteString
BL.readFile FilePath
fname
  Text -> IO Text
forall (m :: * -> *) a. Monad m => a -> m a
return (Text -> IO Text) -> Text -> IO Text
forall a b. (a -> b) -> a -> b
$ TextEncoding -> ByteString -> Text
decode TextEncoding
enc (ByteString -> Text) -> ByteString -> Text
forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
f ByteString
s
#endif

-- | Parse a string containing LP file data.
#if MIN_VERSION_megaparsec(6,0,0)
parseLPString :: (Stream s, Token s ~ Char, IsString (Tokens s)) => FileOptions -> String -> s -> Either (ParseError s) (Problem Scientific)
#else
parseLPString :: (Stream s, Token s ~ Char) => FileOptions -> String -> s -> Either (ParseError s) (Problem Scientific)
#endif
parseLPString :: FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
parseLPString = FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
forall s.
(Stream s, Token s ~ Char, IsString (Tokens s)) =>
FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
LPFile.parseString

-- | Parse a string containing MPS file data.
#if MIN_VERSION_megaparsec(6,0,0)
parseMPSString :: (Stream s, Token s ~ Char, IsString (Tokens s)) => FileOptions -> String -> s -> Either (ParseError s) (Problem Scientific)
#else
parseMPSString :: (Stream s, Token s ~ Char) => FileOptions -> String -> s -> Either (ParseError s) (Problem Scientific)
#endif
parseMPSString :: FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
parseMPSString = FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
forall s.
(Stream s, Token s ~ Char, IsString (Tokens s)) =>
FileOptions
-> FilePath -> s -> Either (ParseError s) (Problem Scientific)
MPSFile.parseString

-- | Generate LP file or MPS file based on file extension.
writeFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeFile FileOptions
opt FilePath
fname Problem Scientific
prob =
  case FilePath -> FilePath
getExt FilePath
fname of
    FilePath
".lp"  -> FileOptions -> FilePath -> Problem Scientific -> IO ()
writeLPFile FileOptions
opt FilePath
fname Problem Scientific
prob
    FilePath
".mps" -> FileOptions -> FilePath -> Problem Scientific -> IO ()
writeMPSFile FileOptions
opt FilePath
fname Problem Scientific
prob
    FilePath
ext -> IOError -> IO ()
forall a. IOError -> IO a
ioError (IOError -> IO ()) -> IOError -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IOError
userError (FilePath -> IOError) -> FilePath -> IOError
forall a b. (a -> b) -> a -> b
$ FilePath
"unknown extension: " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
ext

getExt :: String -> String
getExt :: FilePath -> FilePath
getExt FilePath
fname | (FilePath
base, FilePath
ext) <- FilePath -> (FilePath, FilePath)
splitExtension FilePath
fname =
  case (Char -> Char) -> FilePath -> FilePath
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower FilePath
ext of
#ifdef WITH_ZLIB
    FilePath
".gz" -> FilePath -> FilePath
getExt FilePath
base
#endif
    FilePath
s -> FilePath
s

-- | Generate LP file.
writeLPFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeLPFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeLPFile FileOptions
opt FilePath
fname Problem Scientific
prob =
  case FileOptions -> Problem Scientific -> Either FilePath Text
LPFile.render FileOptions
opt Problem Scientific
prob of
    Left FilePath
err -> IOError -> IO ()
forall a. IOError -> IO a
ioError (IOError -> IO ()) -> IOError -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IOError
userError FilePath
err
    Right Text
s -> FileOptions -> FilePath -> Text -> IO ()
writeTextFile FileOptions
opt FilePath
fname Text
s

-- | Generate MPS file.
writeMPSFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeMPSFile :: FileOptions -> FilePath -> Problem Scientific -> IO ()
writeMPSFile FileOptions
opt FilePath
fname Problem Scientific
prob =
  case FileOptions -> Problem Scientific -> Either FilePath Text
MPSFile.render FileOptions
opt Problem Scientific
prob of
    Left FilePath
err -> IOError -> IO ()
forall a. IOError -> IO a
ioError (IOError -> IO ()) -> IOError -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IOError
userError FilePath
err
    Right Text
s -> FileOptions -> FilePath -> Text -> IO ()
writeTextFile FileOptions
opt FilePath
fname Text
s

writeTextFile :: FileOptions -> FilePath -> TL.Text -> IO ()
writeTextFile :: FileOptions -> FilePath -> Text -> IO ()
writeTextFile FileOptions
opt FilePath
fname Text
s = do
  let writeSimple :: IO ()
writeSimple = do
        FilePath -> IOMode -> (Handle -> IO ()) -> IO ()
forall r. FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile FilePath
fname IOMode
WriteMode ((Handle -> IO ()) -> IO ()) -> (Handle -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Handle
h -> do
          case FileOptions -> Maybe TextEncoding
optFileEncoding FileOptions
opt of
            Maybe TextEncoding
Nothing -> () -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
            Just TextEncoding
enc -> Handle -> TextEncoding -> IO ()
hSetEncoding Handle
h TextEncoding
enc
          Handle -> Text -> IO ()
TLIO.hPutStr Handle
h Text
s
#ifdef WITH_ZLIB
  if FilePath -> CI FilePath
forall s. FoldCase s => s -> CI s
CI.mk (FilePath -> FilePath
takeExtension FilePath
fname) CI FilePath -> CI FilePath -> Bool
forall a. Eq a => a -> a -> Bool
/= CI FilePath
".gz" then do
    IO ()
writeSimple
  else do
    TextEncoding
enc <- case FileOptions -> Maybe TextEncoding
optFileEncoding FileOptions
opt of
             Maybe TextEncoding
Nothing -> IO TextEncoding
getLocaleEncoding
             Just TextEncoding
enc -> TextEncoding -> IO TextEncoding
forall (m :: * -> *) a. Monad m => a -> m a
return TextEncoding
enc
    FilePath -> ByteString -> IO ()
BL.writeFile FilePath
fname (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
GZip.compress (ByteString -> ByteString) -> ByteString -> ByteString
forall a b. (a -> b) -> a -> b
$ TextEncoding -> Text -> ByteString
encode TextEncoding
enc Text
s
#else
  writeSimple
#endif

-- | Generate a 'TL.Text' containing LP file data.
toLPString :: FileOptions -> Problem Scientific -> Either String TL.Text
toLPString :: FileOptions -> Problem Scientific -> Either FilePath Text
toLPString = FileOptions -> Problem Scientific -> Either FilePath Text
LPFile.render

-- | Generate a 'TL.Text' containing MPS file data.
toMPSString :: FileOptions -> Problem Scientific -> Either String TL.Text
toMPSString :: FileOptions -> Problem Scientific -> Either FilePath Text
toMPSString = FileOptions -> Problem Scientific -> Either FilePath Text
MPSFile.render

-- $IO
-- If this library is built with @WithZlib@ flag (enabled by default), 
-- reading/writing gzipped file (@.gz@) are also supported.