{-# 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 '\''