{-# LANGUAGE RecordWildCards #-}
{-| Generate CSV & XLSX files to use with CoinTracking's import feature.

-}
module Web.CoinTracking.Imports
    ( writeImportDataToFile
      -- * Types
    , module Web.CoinTracking.Imports.Types
      -- * CSV Import Files
    , coinTrackingCsvImport
    , headerRow
    , csvEncodingOptions
      -- * XLSX Import Files
    , coinTrackingXlsxImport
    , writeXlsxHeader
    , writeXlsxRow
    ) where

import           Codec.Xlsx                     ( CellValue(..)
                                                , DateBase(DateBase1900)
                                                , Worksheet
                                                , atSheet
                                                , cellValueAt
                                                , dateToNumber
                                                , def
                                                , fromXlsx
                                                )
import           Control.Lens                   ( (.~)
                                                , (?~)
                                                )
import           Data.Char                      ( toLower )
import           Data.Csv                       ( EncodeOptions(..)
                                                , defaultEncodeOptions
                                                , encodeWith
                                                )
import           Data.Foldable                  ( foldl' )
import           Data.Function                  ( (&) )
import           Data.Scientific                ( toRealFloat )
import           Data.Time                      ( zonedTimeToUTC )
import           Data.Time.Clock.POSIX          ( POSIXTime
                                                , getPOSIXTime
                                                )
import           System.FilePath                ( takeExtension )

import           Web.CoinTracking.Imports.Types

import qualified Data.ByteString.Lazy          as LBS
import qualified Data.ByteString.Lazy.Char8    as LBC
import qualified Data.Text                     as T


-- | Write the given data to a file. If the file extension is @.xlsx@ or
-- @.xls@, we write a spreadsheet. Otherwise we write a CSV.
writeImportDataToFile :: FilePath -> [CTImportData] -> IO ()
writeImportDataToFile :: FilePath -> [CTImportData] -> IO ()
writeImportDataToFile FilePath
file [CTImportData]
xs = do
    POSIXTime
currentTime <- IO POSIXTime
getPOSIXTime
    let extension :: FilePath
extension = FilePath -> FilePath
takeExtension FilePath
file
        output :: ByteString
output    = if (Char -> Char) -> FilePath -> FilePath
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower FilePath
extension FilePath -> [FilePath] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [FilePath
".xlsx", FilePath
".xls"]
            then POSIXTime -> [CTImportData] -> ByteString
coinTrackingXlsxImport POSIXTime
currentTime [CTImportData]
xs
            else [CTImportData] -> ByteString
coinTrackingCsvImport [CTImportData]
xs
    FilePath -> ByteString -> IO ()
LBC.writeFile FilePath
file ByteString
output


-- CSVs

-- | Generate the CoinTracking CSV Import for the data, prepended by
-- a header row.
coinTrackingCsvImport :: [CTImportData] -> LBS.ByteString
coinTrackingCsvImport :: [CTImportData] -> ByteString
coinTrackingCsvImport = (ByteString
headerRow ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<>) (ByteString -> ByteString)
-> ([CTImportData] -> ByteString) -> [CTImportData] -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. EncodeOptions -> [CTImportData] -> ByteString
forall a. ToRecord a => EncodeOptions -> [a] -> ByteString
encodeWith EncodeOptions
csvEncodingOptions

-- | The CSV header row to prepend to the generated output.
headerRow :: LBS.ByteString
headerRow :: ByteString
headerRow = EncodeOptions -> [[Text]] -> ByteString
forall a. ToRecord a => EncodeOptions -> [a] -> ByteString
encodeWith
    EncodeOptions
csvEncodingOptions
    [ [ Text
"Type" :: T.Text
      , Text
"Buy"
      , Text
"Cur."
      , Text
"Sell"
      , Text
"Cur."
      , Text
"Fee"
      , Text
"Cur."
      , Text
"Exchange"
      , Text
"Trade-Group"
      , Text
"Comment"
      , Text
"Date"
      , Text
"Tx-ID"
      , Text
"Buy Value in your Account Currency"
      , Text
"Sell Value in your Account Currency"
      ]
    ]

-- | 'defaultEncodeOptions', but with newline-only line endings.
csvEncodingOptions :: EncodeOptions
csvEncodingOptions :: EncodeOptions
csvEncodingOptions = EncodeOptions
defaultEncodeOptions { encUseCrLf :: Bool
encUseCrLf = Bool
False }


-- XLSXs

-- | Generate an XLSX file containing the expected headers rows and the
-- import data.
coinTrackingXlsxImport
    :: POSIXTime
    -- ^ Creation time to embed in the spreadsheet.
    -> [CTImportData]
    -> LBS.ByteString
coinTrackingXlsxImport :: POSIXTime -> [CTImportData] -> ByteString
coinTrackingXlsxImport POSIXTime
createdTime [CTImportData]
rows =
    let sheet :: Worksheet
sheet = (Worksheet -> Int -> CTImportData -> Worksheet)
-> Worksheet -> [CTImportData] -> Worksheet
forall b a. (b -> Int -> a -> b) -> b -> [a] -> b
ixFoldl
            (\Worksheet
sheet_ Int
rowNum CTImportData
row -> Worksheet -> Int -> CTImportData -> Worksheet
writeXlsxRow Worksheet
sheet_ (Int
rowNum Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
3) CTImportData
row)
            (Worksheet -> Worksheet
writeXlsxHeader Worksheet
forall a. Default a => a
def)
            [CTImportData]
rows
        book :: Xlsx
book = Xlsx
forall a. Default a => a
def Xlsx -> (Xlsx -> Xlsx) -> Xlsx
forall a b. a -> (a -> b) -> b
& Text -> Lens' Xlsx (Maybe Worksheet)
atSheet Text
"Sheet1" ((Maybe Worksheet -> Identity (Maybe Worksheet))
 -> Xlsx -> Identity Xlsx)
-> Worksheet -> Xlsx -> Xlsx
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Worksheet
sheet
    in  POSIXTime -> Xlsx -> ByteString
fromXlsx POSIXTime
createdTime Xlsx
book
  where
    -- | Indexed fold from the left.
    ixFoldl :: (b -> Int -> a -> b) -> b -> [a] -> b
    ixFoldl :: (b -> Int -> a -> b) -> b -> [a] -> b
ixFoldl b -> Int -> a -> b
f b
initial =
        (b, Int) -> b
forall a b. (a, b) -> a
fst ((b, Int) -> b) -> ([a] -> (b, Int)) -> [a] -> b
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((b, Int) -> a -> (b, Int)) -> (b, Int) -> [a] -> (b, Int)
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (\(b
b, Int
i) a
a -> (b -> Int -> a -> b
f b
b Int
i a
a, Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1)) (b
initial, Int
0)

-- | Write the standard CoinTracking header to the first two rows of the
-- worksheet.
writeXlsxHeader :: Worksheet -> Worksheet
writeXlsxHeader :: Worksheet -> Worksheet
writeXlsxHeader Worksheet
sheet =
    Worksheet
sheet
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  (Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
1, Int
1)
        ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText
               Text
"CoinTracking Excel Import data (see docs: https://cointracking.info/import/import_xls/)"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
1  Text
"Type"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
2  Text
"Buy Amount"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
3  Text
"Buy Cur."
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
4  Text
"Sell Amount"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
5  Text
"Sell Cur."
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
6  Text
"Feel Amount"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
7  Text
"Fee Cur."
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
8  Text
"Exchange"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
9  Text
"Trade Group"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
10 Text
"Comment"
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
&  Int -> Text -> Worksheet -> Worksheet
writeColumn Int
11 Text
"Date"
  where
    writeColumn :: Int -> T.Text -> Worksheet -> Worksheet
    writeColumn :: Int -> Text -> Worksheet -> Worksheet
writeColumn Int
c Text
t Worksheet
s = Worksheet
s Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& (Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
2, Int
c) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText Text
t


-- | Write a 'CTImportData' to the given row(1-indexed) of the worksheet.
writeXlsxRow :: Worksheet -> Int -> CTImportData -> Worksheet
writeXlsxRow :: Worksheet -> Int -> CTImportData -> Worksheet
writeXlsxRow Worksheet
sheet Int
row CTImportData {Maybe Amount
Text
ZonedTime
CTTransactionType
ctidSellValue :: CTImportData -> Maybe Amount
ctidBuyValue :: CTImportData -> Maybe Amount
ctidTradeId :: CTImportData -> Text
ctidDate :: CTImportData -> ZonedTime
ctidComment :: CTImportData -> Text
ctidGroup :: CTImportData -> Text
ctidExchange :: CTImportData -> Text
ctidFee :: CTImportData -> Maybe Amount
ctidSell :: CTImportData -> Maybe Amount
ctidBuy :: CTImportData -> Maybe Amount
ctidType :: CTImportData -> CTTransactionType
ctidSellValue :: Maybe Amount
ctidBuyValue :: Maybe Amount
ctidTradeId :: Text
ctidDate :: ZonedTime
ctidComment :: Text
ctidGroup :: Text
ctidExchange :: Text
ctidFee :: Maybe Amount
ctidSell :: Maybe Amount
ctidBuy :: Maybe Amount
ctidType :: CTTransactionType
..} =
    Worksheet
sheet
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
1) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText (CTTransactionType -> Text
forall a. IsString a => CTTransactionType -> a
renderTransactionType CTTransactionType
ctidType))
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
2) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderAmount Maybe Amount
ctidBuy)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
3) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderCurrency Maybe Amount
ctidBuy)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
4) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderAmount Maybe Amount
ctidSell)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
5) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderCurrency Maybe Amount
ctidSell)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
6) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderAmount Maybe Amount
ctidFee)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
7) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> Maybe CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a b -> b -> s -> t
.~ Maybe Amount -> Maybe CellValue
renderCurrency Maybe Amount
ctidFee)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
8) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText Text
ctidExchange)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
9) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText Text
ctidGroup)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
10) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ Text -> CellValue
CellText Text
ctidComment)
        Worksheet -> (Worksheet -> Worksheet) -> Worksheet
forall a b. a -> (a -> b) -> b
& ((Int, Int) -> Lens' Worksheet (Maybe CellValue)
cellValueAt (Int
row, Int
11) ((Maybe CellValue -> Identity (Maybe CellValue))
 -> Worksheet -> Identity Worksheet)
-> CellValue -> Worksheet -> Worksheet
forall s t a b. ASetter s t a (Maybe b) -> b -> s -> t
?~ CellValue
renderedDate)
  where
    renderAmount :: Maybe Amount -> Maybe CellValue
    renderAmount :: Maybe Amount -> Maybe CellValue
renderAmount = (Amount -> CellValue) -> Maybe Amount -> Maybe CellValue
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Double -> CellValue
CellDouble (Double -> CellValue) -> (Amount -> Double) -> Amount -> CellValue
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Scientific -> Double
forall a. RealFloat a => Scientific -> a
toRealFloat (Scientific -> Double)
-> (Amount -> Scientific) -> Amount -> Double
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Scientific
aAmount)
    renderCurrency :: Maybe Amount -> Maybe CellValue
    renderCurrency :: Maybe Amount -> Maybe CellValue
renderCurrency = (Amount -> CellValue) -> Maybe Amount -> Maybe CellValue
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Text -> CellValue
CellText (Text -> CellValue) -> (Amount -> Text) -> Amount -> CellValue
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Currency -> Text
cTicker (Currency -> Text) -> (Amount -> Currency) -> Amount -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Currency
aCurrency)
    renderedDate :: CellValue
    renderedDate :: CellValue
renderedDate =
        Double -> CellValue
CellDouble (Double -> CellValue) -> Double -> CellValue
forall a b. (a -> b) -> a -> b
$ DateBase -> UTCTime -> Double
forall a. Fractional a => DateBase -> UTCTime -> a
dateToNumber DateBase
DateBase1900 (UTCTime -> Double) -> UTCTime -> Double
forall a b. (a -> b) -> a -> b
$ ZonedTime -> UTCTime
zonedTimeToUTC ZonedTime
ctidDate