packages feed

xlsx-1.2.0: test/CommonTests/CellRefTests.hs

{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE ViewPatterns          #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module CommonTests.CellRefTests
  ( tests
  ) where

import qualified Control.Applicative as Alt
import Control.Monad
import Data.Char (chr, isPrint)
import qualified Data.List as L
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word8)
import Test.SmallCheck.Series as Series (NonEmpty (..), Positive (..),
                                         Serial (..), cons1, cons2, generate,
                                         (\/))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.SmallCheck (testProperty)

import Codec.Xlsx.Types.Common

tests :: TestTree
tests =
  testGroup
    "Types.Common CellRef tests"
  [ testProperty "textToColumnIndex . columnIndexToText == id" $
        \(Positive i) -> i == textToColumnIndex (columnIndexToText i)

    , testProperty "row2coord . coord2row = id" $
        \(r :: RowCoord) -> r == row2coord (coord2row r)

    , testProperty "col2coord . coord2col = id" $
        \(c :: ColumnCoord) -> c == col2coord (coord2col c)

    , testProperty "fromSingleCellRef' . singleCellRef' = pure" $
        \(cellCoord :: CellCoord) -> pure cellCoord == fromSingleCellRef' (singleCellRef' cellCoord)

    , testProperty "fromRange' . mkRange' = pure" $
        \(range :: RangeCoord) -> pure range == fromRange' (uncurry mkRange' range)

    , testProperty "fromForeignSingleCellRef . mkForeignSingleCellRef = pure" $
        \(viewForeignCellParams -> params) ->
            pure params == fromForeignSingleCellRef (uncurry mkForeignSingleCellRef params)

    , testProperty "fromSingleCellRef' . mkForeignSingleCellRef = pure . snd" $
        \(viewForeignCellParams -> (nStr, cellCoord)) ->
            pure cellCoord == fromSingleCellRef' (mkForeignSingleCellRef nStr cellCoord)

    , testProperty "fromForeignSingleCellRef . singleCellRef' = const empty" $
        \(cellCoord :: CellCoord) ->
            Alt.empty == fromForeignSingleCellRef (singleCellRef' cellCoord)

    , testProperty "fromForeignRange . mkForeignRange = pure" $
        \(viewForeignRangeParams -> params@(nStr, (start, end))) ->
            pure params == fromForeignRange (mkForeignRange nStr start end)

    , testProperty "fromRange' . mkForeignRange = pure . snd" $
        \(viewForeignRangeParams -> (nStr, range@(start, end))) ->
            pure range == fromRange' (mkForeignRange nStr start end)

    , testProperty "fromForeignRange . mkRange' = const empty" $
        \(range :: RangeCoord) ->
            Alt.empty == fromForeignRange (uncurry mkRange' range)

    , testCase "building single CellRefs" $ do
        singleCellRef' (RowRel 5, ColumnRel 25) @?= CellRef "Y5"
        singleCellRef' (RowRel 5, ColumnAbs 25) @?= CellRef "$Y5"
        singleCellRef' (RowAbs 5, ColumnRel 25) @?= CellRef "Y$5"
        singleCellRef' (RowAbs 5, ColumnAbs 25) @?= CellRef "$Y$5"
        singleCellRef (5, 25) @?= CellRef "Y5"
    , testCase "parsing single CellRefs as abstract coordinates" $ do
        fromSingleCellRef (CellRef "Y5") @?= Just (5, 25)
        fromSingleCellRef (CellRef "$Y5") @?= Just (5, 25)
        fromSingleCellRef (CellRef "Y$5") @?= Just (5, 25)
        fromSingleCellRef (CellRef "$Y$5") @?= Just (5, 25)
    , testCase "parsing single CellRefs as potentially absolute coordinates" $ do
        fromSingleCellRef' (CellRef "Y5") @?= Just (RowRel 5, ColumnRel 25)
        fromSingleCellRef' (CellRef "$Y5") @?= Just (RowRel 5, ColumnAbs 25)
        fromSingleCellRef' (CellRef "Y$5") @?= Just (RowAbs 5, ColumnRel 25)
        fromSingleCellRef' (CellRef "$Y$5") @?= Just (RowAbs 5, ColumnAbs 25)
        fromSingleCellRef' (CellRef "$Y$50") @?= Just (RowAbs 50, ColumnAbs 25)
        fromSingleCellRef' (CellRef "$Y$5$0") @?= Nothing
        fromSingleCellRef' (CellRef "Y5:Z10") @?= Nothing
    , testCase "building ranges" $ do
        mkRange (5, 25) (10, 26) @?= CellRef "Y5:Z10"
        mkRange' (RowRel 5, ColumnRel 25) (RowRel 10, ColumnRel 26) @?= CellRef "Y5:Z10"
        mkRange' (RowAbs 5, ColumnAbs 25) (RowAbs 10, ColumnAbs 26) @?= CellRef "$Y$5:$Z$10"
        mkRange' (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "$Y5:Z$10"
        mkForeignRange "myWorksheet" (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "'myWorksheet'!$Y5:Z$10"
        mkForeignRange "my sheet" (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "'my sheet'!$Y5:Z$10"
    , testCase "parsing ranges CellRefs as abstract coordinates" $ do
        fromRange (CellRef "Y5:Z10") @?= Just ((5, 25), (10, 26))
        fromRange (CellRef "$Y$5:$Z$10") @?= Just ((5, 25), (10, 26))
        fromRange (CellRef "myWorksheet!$Y5:Z$10") @?= Just ((5, 25), (10, 26))
    , testCase "parsing ranges CellRefs as potentially absolute coordinates" $ do
        fromRange' (CellRef "Y5:Z10") @?= Just ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26))
        fromRange' (CellRef "$Y$5:$Z$10") @?= Just ((RowAbs 5, ColumnAbs 25), (RowAbs 10, ColumnAbs 26))
        fromRange' (CellRef "myWorksheet!$Y5:Z$10") @?= Just ((RowRel 5, ColumnAbs 25), (RowAbs 10, ColumnRel 26))
        fromForeignRange (CellRef "myWorksheet!$Y5:Z$10") @?= Just ("myWorksheet", ((RowRel 5, ColumnAbs 25), (RowAbs 10, ColumnRel 26)))
        fromForeignRange (CellRef "'myWorksheet'!Y5:Z10") @?= Just ("myWorksheet", ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26)))
        fromForeignRange (CellRef "'my sheet'!Y5:Z10") @?= Just ("my sheet", ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26)))
        fromForeignRange (CellRef "$Y5:Z$10") @?= Nothing
  ]

instance Monad m => Serial m RowIndex where
  series = cons1 (RowIndex . getPositive)

instance Monad m => Serial m ColumnIndex where
  series = cons1 (ColumnIndex . getPositive)

instance Monad m => Serial m RowCoord where
  series = cons1 (RowAbs . getPositive) \/ cons1 (RowRel . getPositive)

instance Monad m => Serial m ColumnCoord where
  series = cons1 (ColumnAbs . getPositive) \/ cons1 (ColumnRel . getPositive)

-- | Allow defining an instance to generate valid foreign range params
data MkForeignRangeRef =
  MkForeignRangeRef NameString RangeCoord
  deriving (Show)

viewForeignRangeParams :: MkForeignRangeRef -> (Text, RangeCoord)
viewForeignRangeParams (MkForeignRangeRef nameStr range) = (nameString nameStr, range)

instance Monad m => Serial m MkForeignRangeRef where
  series = cons2 MkForeignRangeRef

-- | Allow defining an instance to generate valid foreign cellref params
data MkForeignCellRef =
  MkForeignCellRef NameString CellCoord
  deriving (Show)

viewForeignCellParams :: MkForeignCellRef -> (Text, CellCoord)
viewForeignCellParams (MkForeignCellRef nameStr coord) = (nameString nameStr, coord)

instance Monad m => Serial m MkForeignCellRef where
  series = cons2 MkForeignCellRef

-- | Overload an instance for allowed sheet name chars
newtype NameChar =
  NameChar { _unNCh :: Char }
  deriving (Show, Eq)

-- | Instance for Char which broadens the pool to permitted ascii and some wchars
-- as @Serial m Char@ is only @[ 'A' .. 'Z' ]@
instance Monad m => Serial m NameChar where
  series =
    fmap NameChar $
      let wChars = ['é', '€', '愛']
          startSeq = "ab cd'" -- single quote is permitted as long as it's not 1st or last
          authorizedAscii =
            let asciiRange = L.map (chr . fromIntegral) [minBound @Word8 .. maxBound `div` 2]
                forbiddenClass = "[]*:?/\\" :: String
                isAuthorized c = isPrint c && not (c `L.elem` forbiddenClass)
                isPlanned c = isAuthorized c && not (c `L.elem` startSeq)
              in L.filter isPlanned asciiRange
        in generate $ \d -> take (d + 1) $ startSeq ++ wChars ++ authorizedAscii

-- | Allow defining an instance to generate valid sheetnames (non-empty, valid char set, squote rule)
newtype NameString =
  NameString { _unNS :: Series.NonEmpty NameChar }
  deriving (Show)

nameString :: NameString -> Text
nameString = T.pack . L.map _unNCh . getNonEmpty . _unNS

instance Monad m => Serial m NameString where
  series = fmap NameString $
    series @m @(Series.NonEmpty NameChar) >>= \t -> t <$ guard (isOk t)
        where
          -- squote isn't permitted at the beginning and the end of a sheet's name
          isOk (getNonEmpty -> s) =
            head s /= NameChar '\'' &&
            last s /= NameChar '\''