packages feed

cointracking-imports-0.1.0.2: src/Web/CoinTracking/Imports.hs

{-# 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 file xs = do
    currentTime <- getPOSIXTime
    let extension = takeExtension file
        output    = if map toLower extension `elem` [".xlsx", ".xls"]
            then coinTrackingXlsxImport currentTime xs
            else coinTrackingCsvImport xs
    LBC.writeFile file output


-- CSVs

-- | Generate the CoinTracking CSV Import for the data, prepended by
-- a header row.
--
-- Note: the resulting 'LBS.ByteString' has it's final newline removed, as
-- CoinTracking's Import creates a double entry with newlines at the end of
-- an import file.
coinTrackingCsvImport :: [CTImportData] -> LBS.ByteString
coinTrackingCsvImport =
    (headerRow <>) . LBC.init . encodeWith csvEncodingOptions

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

-- | 'defaultEncodeOptions', but with newline-only line endings.
csvEncodingOptions :: EncodeOptions
csvEncodingOptions = defaultEncodeOptions { encUseCrLf = 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 createdTime rows =
    let sheet = ixFoldl
            (\sheet_ rowNum row -> writeXlsxRow sheet_ (rowNum + 3) row)
            (writeXlsxHeader def)
            rows
        book = def & atSheet "Sheet1" ?~ sheet
    in  fromXlsx createdTime book
  where
    -- | Indexed fold from the left.
    ixFoldl :: (b -> RowIndex -> a -> b) -> b -> [a] -> b
    ixFoldl f initial =
        fst . foldl' (\(b, i) a -> (f b i a, i + 1)) (initial, 0)

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


-- | Write a 'CTImportData' to the given row(1-indexed) of the worksheet.
writeXlsxRow :: Worksheet -> RowIndex -> CTImportData -> Worksheet
writeXlsxRow sheet row CTImportData {..} =
    sheet
        & (cellValueAt (row, 1) ?~ CellText (renderTransactionType ctidType))
        & (cellValueAt (row, 2) .~ renderAmount ctidBuy)
        & (cellValueAt (row, 3) .~ renderCurrency ctidBuy)
        & (cellValueAt (row, 4) .~ renderAmount ctidSell)
        & (cellValueAt (row, 5) .~ renderCurrency ctidSell)
        & (cellValueAt (row, 6) .~ renderAmount ctidFee)
        & (cellValueAt (row, 7) .~ renderCurrency ctidFee)
        & (cellValueAt (row, 8) ?~ CellText ctidExchange)
        & (cellValueAt (row, 9) ?~ CellText ctidGroup)
        & (cellValueAt (row, 10) ?~ CellText ctidComment)
        & (cellValueAt (row, 11) ?~ renderedDate)
  where
    renderAmount :: Maybe Amount -> Maybe CellValue
    renderAmount = fmap (CellDouble . toRealFloat . aAmount)
    renderCurrency :: Maybe Amount -> Maybe CellValue
    renderCurrency = fmap (CellText . cTicker . aCurrency)
    renderedDate :: CellValue
    renderedDate =
        CellDouble $ dateToNumber DateBase1900 $ zonedTimeToUTC ctidDate