1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
{-# 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 '\''
|