{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE QuasiQuotes #-} module Main (main) where import Control.Lens import Control.Monad.State.Lazy import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as LB import Data.Map (Map) import qualified Data.Map as M import Data.Maybe (mapMaybe) import qualified Data.Text as T import Data.Time.Clock.POSIX (POSIXTime) import qualified Data.Vector as V import Text.RawString.QQ import Text.XML import Text.XML.Cursor import Test.Tasty (defaultMain, testGroup) import Test.Tasty.HUnit (testCase) import Test.Tasty.SmallCheck (testProperty) import Test.SmallCheck.Series (Positive (..)) import Test.Tasty.HUnit (HUnitFailure (..), (@=?)) import Codec.Xlsx import Codec.Xlsx.Formatted import Codec.Xlsx.Parser.Internal import Codec.Xlsx.Parser.Internal.PivotTable import Codec.Xlsx.Types.Internal import Codec.Xlsx.Types.Internal.CommentTable import Codec.Xlsx.Types.Internal.CustomProperties as CustomProperties import Codec.Xlsx.Types.Internal.SharedStringTable import Codec.Xlsx.Types.PivotTable.Internal import Codec.Xlsx.Types.StyleSheet import Codec.Xlsx.Writer.Internal import Codec.Xlsx.Writer.Internal.PivotTable import Diff main :: IO () main = defaultMain $ testGroup "Tests" [ testProperty "col2int . int2col == id" $ \(Positive i) -> i == col2int (int2col i) , testCase "write . read == id" $ do let bs = fromXlsx testTime testXlsx LB.writeFile "data-test.xlsx" bs testXlsx @==? toXlsx (fromXlsx testTime testXlsx) , testCase "fromRows . toRows == id" $ testCellMap1 @=? fromRows (toRows testCellMap1) , testCase "fromRight . parseStyleSheet . renderStyleSheet == id" $ testStyleSheet @==? fromRight (parseStyleSheet (renderStyleSheet testStyleSheet)) , testCase "correct shared strings parsing" $ [testSharedStringTable] @=? parseBS testStrings , testCase "correct shared strings parsing even when one of the shared strings entry is just " $ [testSharedStringTableWithEmpty] @=? parseBS testStringsWithEmpty , testCase "correct comments parsing" $ [testCommentTable] @=? parseBS testComments , testCase "correct drawing parsing" $ [testDrawing] @==? parseBS testDrawingFile , testCase "write . read == id for Drawings" $ [testDrawing] @==? parseBS testWrittenDrawing , testCase "correct chart parsing" $ [testChartSpace] @==? parseBS testChartFile , testCase "write . read == id for Charts" $ [testChartSpace] @==? parseBS testWrittenChartSpace , testCase "correct custom properties parsing" $ [testCustomProperties] @==? parseBS testCustomPropertiesXml , testCase "proper results from `formatted`" $ testFormattedResult @==? testRunFormatted , testCase "formatted . toFormattedCells = id" $ do let fmtd = formatted testFormattedCells minimalStyleSheet testFormattedCells @==? toFormattedCells (formattedCellMap fmtd) (formattedMerges fmtd) (formattedStyleSheet fmtd) , testCase "proper results from `conditionalltyFormatted`" $ testCondFormattedResult @==? testRunCondFormatted , testCase "toXlsxEither: properly formatted" $ Right testXlsx @==? toXlsxEither (fromXlsx testTime testXlsx) , testCase "toXlsxEither: invalid format" $ Left InvalidZipArchive @==? toXlsxEither "this is not a valid XLSX file" , testCase "proper pivot table rendering" $ do let ptFiles = renderPivotTableFiles 3 testPivotTable parseLBS_ def (pvtfTable ptFiles) @==? stripContentSpaces (parseLBS_ def testPivotTableDefinition) parseLBS_ def (pvtfCacheDefinition ptFiles) @==? stripContentSpaces (parseLBS_ def testPivotCacheDefinition) , testCase "proper pivot table parsing" $ do let sheetName = "Sheet1" ref = CellRef "A1:D5" fields = [ PivotFieldName "Color" , PivotFieldName "Year" , PivotFieldName "Price" , PivotFieldName "Count" ] forCacheId (CacheId 3) = Just (sheetName, ref, fields) forCacheId _ = Nothing Just (sheetName, ref, fields) @==? parseCache testPivotCacheDefinition Just testPivotTable @==? parsePivotTable forCacheId testPivotTableDefinition ] parseBS :: FromCursor a => ByteString -> [a] parseBS = fromCursor . fromDocument . parseLBS_ def testXlsx :: Xlsx testXlsx = Xlsx sheets minimalStyles definedNames customProperties where sheets = [("List1", sheet1), ("Another sheet", sheet2), ("with pivot table", pvSheet)] sheet1 = Worksheet cols rowProps testCellMap1 drawing ranges sheetViews pageSetup cFormatting validations [] (Just autoFilter) tables (Just protection) autoFilter = def & afRef ?~ CellRef "A1:E10" & afFilterColumns .~ fCols fCols = M.fromList [ (1, Filters ["a","b","ZZZ"]) , (2, CustomFiltersAnd (CustomFilter FltrGreaterThanOrEqual "0") (CustomFilter FltrLessThan "42"))] tables = [ Table { tblName = "Table1" , tblDisplayName = "Table1" , tblRef = CellRef "A3" , tblColumns = [TableColumn "another text"] , tblAutoFilter = Just (def & afRef ?~ CellRef "A3") } ] protection = fullSheetProtection { _sprScenarios = False , _sprLegacyPassword = Just $ legacyPassword "hard password" } sheet2 = def & wsCells .~ testCellMap2 pvSheet = sheetWithPvCells & wsPivotTables .~ [testPivotTable] sheetWithPvCells = flip execState def $ do forM_ (zip [1..] ["Color", "Year", "Price", "Count"]) $ \(c, n) -> cellValueAt (1, c) ?= CellText n cellValueAt (2, 1) ?= CellText "brown" cellValueAt (2, 2) ?= CellDouble 2016 cellValueAt (2, 3) ?= CellDouble 12.34 cellValueAt (2, 4) ?= CellDouble 42 rowProps = M.fromList [(1, RowProps (Just 50) (Just 3))] cols = [ColumnsWidth 1 10 15 (Just 1)] drawing = Just $ testDrawing { _xdrAnchors = map resolve $ _xdrAnchors testDrawing } resolve :: Anchor RefId RefId -> Anchor FileInfo ChartSpace resolve Anchor {..} = let obj = case _anchObject of Picture {..} -> let picBlipFill = (_picBlipFill & bfpImageInfo ?~ fileInfo) in Picture { _picMacro = _picMacro , _picPublished = _picPublished , _picNonVisual = _picNonVisual , _picBlipFill = picBlipFill , _picShapeProperties = _picShapeProperties } Graphic nv _ tr -> Graphic nv testChartSpace tr in Anchor { _anchAnchoring = _anchAnchoring , _anchObject = obj , _anchClientData = _anchClientData } fileInfo = FileInfo "dummy.png" "image/png" "fake contents" ranges = [mkRange (1,1) (1,2), mkRange (2,2) (10, 5)] minimalStyles = renderStyleSheet minimalStyleSheet definedNames = DefinedNames [("SampleName", Nothing, "A10:A20")] sheetViews = Just [sheetView1, sheetView2] sheetView1 = def & sheetViewRightToLeft ?~ True & sheetViewTopLeftCell ?~ CellRef "B5" sheetView2 = def & sheetViewType ?~ SheetViewTypePageBreakPreview & sheetViewWorkbookViewId .~ 5 & sheetViewSelection .~ [ def & selectionActiveCell ?~ CellRef "C2" & selectionPane ?~ PaneTypeBottomRight , def & selectionActiveCellId ?~ 1 & selectionSqref ?~ SqRef [ CellRef "A3:A10" , CellRef "B1:G3"] ] pageSetup = Just $ def & pageSetupBlackAndWhite ?~ True & pageSetupCopies ?~ 2 & pageSetupErrors ?~ PrintErrorsDash & pageSetupPaperSize ?~ PaperA4 customProperties = M.fromList [("some_prop", VtInt 42)] cFormatting = M.fromList [(SqRef [CellRef "A1:B3"], rules1), (SqRef [CellRef "C1:C10"], rules2)] cfRule c d = CfRule { _cfrCondition = c , _cfrDxfId = Just d , _cfrPriority = topCfPriority , _cfrStopIfTrue = Nothing } rules1 = [ cfRule ContainsBlanks 1 , cfRule (ContainsText "foo") 2 , cfRule (CellIs (OpBetween (Formula "A1") (Formula "B10"))) 3 ] rules2 = [ cfRule ContainsErrors 3 ] testCellMap1 :: CellMap testCellMap1 = M.fromList [ ((1, 2), cd1_2), ((1, 5), cd1_5) , ((3, 1), cd3_1), ((3, 2), cd3_2), ((3, 7), cd3_7) , ((4, 1), cd4_1), ((4, 2), cd4_2), ((4, 3), cd4_3) ] where cd v = def {_cellValue=Just v} cd1_2 = cd (CellText "just a text") cd1_5 = cd (CellDouble 42.4567) cd3_1 = cd (CellText "another text") cd3_2 = def -- shouldn't it be skipped? cd3_7 = cd (CellBool True) cd4_1 = cd (CellDouble 1) cd4_2 = cd (CellDouble 2) cd4_3 = (cd (CellDouble (1+2))) { _cellFormula = Just $ simpleCellFormula "A4+B4" } testCellMap2 :: CellMap testCellMap2 = M.fromList [ ((1, 2), def & cellValue ?~ CellText "something here") , ((3, 5), def & cellValue ?~ CellDouble 123.456) , ((2, 4), def & cellValue ?~ CellText "value" & cellComment ?~ comment1 ) , ((10, 7), def & cellValue ?~ CellText "value" & cellComment ?~ comment2 ) , ((11, 4), def & cellComment ?~ comment3) ] where comment1 = Comment (XlsxText "simple comment") "bob" True comment2 = Comment (XlsxRichText [rich1, rich2]) "alice" False comment3 = Comment (XlsxText "comment for an empty cell") "bob" True rich1 = def & richTextRunText.~ "Look ma!" & richTextRunProperties ?~ ( def & runPropertiesBold ?~ True & runPropertiesFont ?~ "Tahoma") rich2 = def & richTextRunText .~ "It's blue!" & richTextRunProperties ?~ ( def & runPropertiesItalic ?~ True & runPropertiesColor ?~ (def & colorARGB ?~ "FF000080")) testTime :: POSIXTime testTime = 123 fromRight :: Either a b -> b fromRight (Right b) = b testStyleSheet :: StyleSheet testStyleSheet = minimalStyleSheet & styleSheetDxfs .~ [dxf1, dxf2] & styleSheetNumFmts .~ M.fromList [(164, "0.000")] & styleSheetCellXfs %~ (++ [cellXf1, cellXf2]) where dxf1 = def & dxfFont ?~ (def & fontBold ?~ True & fontSize ?~ 12) dxf2 = def & dxfFill ?~ (def & fillPattern ?~ (def & fillPatternBgColor ?~ red)) red = def & colorARGB ?~ "FFFF0000" cellXf1 = def { _cellXfApplyNumberFormat = Just True , _cellXfNumFmtId = Just 2 } cellXf2 = def { _cellXfApplyNumberFormat = Just True , _cellXfNumFmtId = Just 164 } testSharedStringTable :: SharedStringTable testSharedStringTable = SharedStringTable $ V.fromList items where items = [text, rich] text = XlsxText "plain text" rich = XlsxRichText [ RichTextRun Nothing "Just " , RichTextRun (Just props) "example" ] props = def & runPropertiesBold .~ Just True & runPropertiesUnderline .~ Just FontUnderlineSingle & runPropertiesSize .~ Just 10 & runPropertiesFont .~ Just "Arial" & runPropertiesFontFamily .~ Just FontFamilySwiss testSharedStringTableWithEmpty :: SharedStringTable testSharedStringTableWithEmpty = SharedStringTable $ V.fromList [XlsxText ""] testCommentTable = CommentTable $ M.fromList [ (CellRef "D4", Comment (XlsxRichText rich) "Bob" True) , (CellRef "A2", Comment (XlsxText "Some comment here") "CBR" True) ] where rich = [ RichTextRun { _richTextRunProperties = Just $ def & runPropertiesBold ?~ True & runPropertiesCharset ?~ 1 & runPropertiesColor ?~ def -- TODO: why not Nothing here? & runPropertiesFont ?~ "Calibri" & runPropertiesScheme ?~ FontSchemeMinor & runPropertiesSize ?~ 8.0 , _richTextRunText = "Bob:"} , RichTextRun { _richTextRunProperties = Just $ def & runPropertiesCharset ?~ 1 & runPropertiesColor ?~ def & runPropertiesFont ?~ "Calibri" & runPropertiesScheme ?~ FontSchemeMinor & runPropertiesSize ?~ 8.0 , _richTextRunText = "Why such high expense?"}] testStrings :: ByteString testStrings = "\ \\ \plain text\ \Just \ \example\ \" testStringsWithEmpty :: ByteString testStringsWithEmpty = "\ \\ \\ \" testComments :: ByteString testComments = [r| Bob CBR Bob: Why such high expense? Some comment here |] testDrawing :: UnresolvedDrawing testDrawing = Drawing [anchor1, anchor2] where anchor1 = Anchor {_anchAnchoring = anchoring1, _anchObject = pic, _anchClientData = def} anchoring1 = TwoCellAnchor { tcaFrom = unqMarker (0, 0) (0, 0) , tcaTo = unqMarker (12, 320760) (33, 38160) , tcaEditAs = EditAsAbsolute } pic = Picture { _picMacro = Nothing , _picPublished = False , _picNonVisual = nonVis1 , _picBlipFill = bfProps , _picShapeProperties = shProps } nonVis1 = PicNonVisual $ NonVisualDrawingProperties { _nvdpId = DrawingElementId 0 , _nvdpName = "Picture 1" , _nvdpDescription = Just "" , _nvdpHidden = False , _nvdpTitle = Nothing } bfProps = BlipFillProperties {_bfpImageInfo = Just (RefId "rId1"), _bfpFillMode = Just FillStretch} shProps = ShapeProperties { _spXfrm = Just trnsfrm , _spGeometry = Just PresetGeometry , _spFill = Nothing , _spOutline = Just $ LineProperties (Just NoFill) } trnsfrm = Transform2D { _trRot = Angle 0 , _trFlipH = False , _trFlipV = False , _trOffset = Just (unqPoint2D 0 0) , _trExtents = Just (PositiveSize2D (PositiveCoordinate 10074240) (PositiveCoordinate 5402520)) } anchor2 = Anchor { _anchAnchoring = anchoring2 , _anchObject = graphic , _anchClientData = def } anchoring2 = TwoCellAnchor { tcaFrom = unqMarker (0, 87840) (21, 131040) , tcaTo = unqMarker (7, 580320) (38, 132480) , tcaEditAs = EditAsOneCell } graphic = Graphic { _grNonVisual = nonVis2 , _grChartSpace = RefId "rId2" , _grTransform = transform } nonVis2 = GraphNonVisual $ NonVisualDrawingProperties { _nvdpId = DrawingElementId 1 , _nvdpName = "" , _nvdpDescription = Nothing , _nvdpHidden = False , _nvdpTitle = Nothing } transform = Transform2D { _trRot = Angle 0 , _trFlipH = False , _trFlipV = False , _trOffset = Just (unqPoint2D 0 0) , _trExtents = Just (PositiveSize2D (PositiveCoordinate 10074240) (PositiveCoordinate 5402520)) } testDrawingFile :: ByteString testDrawingFile = [r| 00 00 12320760 3338160 087840 21131040 7580320 38132480 |] testWrittenDrawing :: ByteString testWrittenDrawing = renderLBS def $ toDocument testDrawing testCustomProperties :: CustomProperties testCustomProperties = CustomProperties.fromList [ ("testTextProp", VtLpwstr "test text property value") , ("prop2", VtLpwstr "222") , ("bool", VtBool False) , ("prop333", VtInt 1) , ("decimal", VtDecimal 1.234) ] testCustomPropertiesXml :: ByteString testCustomPropertiesXml = [r| 222 1 test text property value 1.234 false ZXhhbXBs ZSBibG9i IGNvbnRl bnRz |] testFormattedResult :: Formatted testFormattedResult = Formatted cm styleSheet merges where cm = M.fromList [ ((1, 1), cell11) , ((1, 2), cell12) , ((2, 5), cell25) ] cell11 = Cell { _cellStyle = Just 1 , _cellValue = Just (CellText "text at A1") , _cellComment = Nothing , _cellFormula = Nothing } cell12 = Cell { _cellStyle = Just 2 , _cellValue = Just (CellDouble 1.23) , _cellComment = Nothing , _cellFormula = Nothing } cell25 = Cell { _cellStyle = Just 3 , _cellValue = Just (CellDouble 1.23456) , _cellComment = Nothing , _cellFormula = Nothing } merges = [] styleSheet = minimalStyleSheet & styleSheetCellXfs %~ (++ [cellXf1, cellXf2, cellXf3]) & styleSheetFonts %~ (++ [font1, font2]) & styleSheetNumFmts .~ numFmts nextFontId = length (minimalStyleSheet ^. styleSheetFonts) cellXf1 = def { _cellXfApplyFont = Just True , _cellXfFontId = Just nextFontId } font1 = def { _fontName = Just "Calibri" , _fontBold = Just True } cellXf2 = def { _cellXfApplyFont = Just True , _cellXfFontId = Just (nextFontId + 1) , _cellXfApplyNumberFormat = Just True , _cellXfNumFmtId = Just 164 } font2 = def { _fontItalic = Just True } cellXf3 = def { _cellXfApplyNumberFormat = Just True , _cellXfNumFmtId = Just 2 } numFmts = M.fromList [(164, "0.0000")] testRunFormatted :: Formatted testRunFormatted = formatted formattedCellMap minimalStyleSheet where formattedCellMap = flip execState def $ do let font1 = def & fontBold ?~ True & fontName ?~ "Calibri" at (1, 1) ?= (def & formattedCell . cellValue ?~ CellText "text at A1" & formattedFormat . formatFont ?~ font1) at (1, 2) ?= (def & formattedCell . cellValue ?~ CellDouble 1.23 & formattedFormat . formatFont . non def . fontItalic ?~ True & formattedFormat . formatNumberFormat ?~ fmtDecimalsZeroes 4) at (2, 5) ?= (def & formattedCell . cellValue ?~ CellDouble 1.23456 & formattedFormat . formatNumberFormat ?~ StdNumberFormat Nf2Decimal) testCondFormattedResult :: CondFormatted testCondFormattedResult = CondFormatted styleSheet formattings where styleSheet = minimalStyleSheet & styleSheetDxfs .~ dxfs dxfs = [ def & dxfFont ?~ (def & fontUnderline ?~ FontUnderlineSingle) , def & dxfFont ?~ (def & fontStrikeThrough ?~ True) , def & dxfFont ?~ (def & fontBold ?~ True) ] formattings = M.fromList [ (SqRef [CellRef "A1:A2", CellRef "B2:B3"], [cfRule1, cfRule2]) , (SqRef [CellRef "C3:E10"], [cfRule1]) , (SqRef [CellRef "F1:G10"], [cfRule3]) ] cfRule1 = CfRule { _cfrCondition = ContainsBlanks , _cfrDxfId = Just 0 , _cfrPriority = 1 , _cfrStopIfTrue = Nothing } cfRule2 = CfRule { _cfrCondition = BeginsWith "foo" , _cfrDxfId = Just 1 , _cfrPriority = 1 , _cfrStopIfTrue = Nothing } cfRule3 = CfRule { _cfrCondition = CellIs (OpGreaterThan (Formula "A1")) , _cfrDxfId = Just 2 , _cfrPriority = 1 , _cfrStopIfTrue = Nothing } testFormattedCells :: Map (Int, Int) FormattedCell testFormattedCells = flip execState def $ do at (1,1) ?= (def & formattedRowSpan .~ 5 & formattedColSpan .~ 5 & formattedFormat . formatBorder . non def . borderTop . non def . borderStyleLine ?~ LineStyleDashed & formattedFormat . formatBorder . non def . borderBottom . non def . borderStyleLine ?~ LineStyleDashed) at (10,2) ?= (def & formattedFormat . formatFont . non def . fontBold ?~ True) testRunCondFormatted :: CondFormatted testRunCondFormatted = conditionallyFormatted condFmts minimalStyleSheet where condFmts = flip execState def $ do let cfRule1 = def & condfmtCondition .~ ContainsBlanks & condfmtDxf . dxfFont . non def . fontUnderline ?~ FontUnderlineSingle cfRule2 = def & condfmtCondition .~ BeginsWith "foo" & condfmtDxf . dxfFont . non def . fontStrikeThrough ?~ True cfRule3 = def & condfmtCondition .~ CellIs (OpGreaterThan (Formula "A1")) & condfmtDxf . dxfFont . non def . fontBold ?~ True at (CellRef "A1:A2") ?= [cfRule1, cfRule2] at (CellRef "B2:B3") ?= [cfRule1, cfRule2] at (CellRef "C3:E10") ?= [cfRule1] at (CellRef "F1:G10") ?= [cfRule3] testChartFile :: ByteString testChartFile = [r| Line chart title Sheet1!$A$1 Sheet1!$B$1:$D$1 Sheet1!$A$2 Sheet1!$B$2:$D$2 |] testChartSpace :: ChartSpace testChartSpace = ChartSpace { _chspTitle = Just $ ChartTitle titleBody , _chspCharts = charts , _chspLegend = Nothing , _chspPlotVisOnly = Just True , _chspDispBlanksAs = Just DispBlanksAsGap } where titleBody = TextBody { _txbdRotation = Angle 0 , _txbdSpcFirstLastPara = False , _txbdVertOverflow = TextVertOverflow , _txbdVertical = TextVerticalHorz , _txbdWrap = TextWrapSquare , _txbdAnchor = TextAnchoringBottom , _txbdAnchorCenter = False , _txbdParagraphs = [TextParagraph Nothing [RegularRun Nothing "Line chart title"]] } charts = [ LineChart { _lnchGrouping = StandardGrouping , _lnchSeries = series , _lnchMarker = Just False , _lnchSmooth = Just False } ] series = [ LineSeries { _lnserShared = Series . Just $ Formula "Sheet1!$A$1" , _lnserMarker = Just markerNone , _lnserDataLblProps = Nothing , _lnserVal = Just $ Formula "Sheet1!$B$1:$D$1" , _lnserSmooth = Just False } , LineSeries { _lnserShared = Series . Just $ Formula "Sheet1!$A$2" , _lnserMarker = Just markerNone , _lnserDataLblProps = Nothing , _lnserVal = Just $ Formula "Sheet1!$B$2:$D$2" , _lnserSmooth = Just False } ] markerNone = DataMarker {_dmrkSymbol = Just DataMarkerNone, _dmrkSize = Nothing} testWrittenChartSpace :: ByteString testWrittenChartSpace = renderLBS def{rsNamespaces=nss} $ toDocument testChartSpace where nss = [ ("c", "http://schemas.openxmlformats.org/drawingml/2006/chart") , ("a", "http://schemas.openxmlformats.org/drawingml/2006/main") ] validations :: Map SqRef DataValidation validations = M.fromList [ ( SqRef [CellRef "A1"], def ) , ( SqRef [CellRef "A1", CellRef "B2:C3"], def { _dvAllowBlank = True , _dvError = Just "incorrect data" , _dvErrorStyle = ErrorStyleInformation , _dvErrorTitle = Just "error title" , _dvPrompt = Just "enter data" , _dvPromptTitle = Just "prompt title" , _dvShowDropDown = True , _dvShowErrorMessage = True , _dvShowInputMessage = True , _dvValidationType = ValidationTypeList ["aaaa","bbbb","cccc"] } ) , ( SqRef [CellRef "A6", CellRef "I2"], def { _dvAllowBlank = False , _dvError = Just "aaa" , _dvErrorStyle = ErrorStyleWarning , _dvErrorTitle = Just "bbb" , _dvPrompt = Just "ccc" , _dvPromptTitle = Just "ddd" , _dvShowDropDown = False , _dvShowErrorMessage = False , _dvShowInputMessage = False , _dvValidationType = ValidationTypeDecimal $ ValGreaterThan $ Formula "10" } ) , ( SqRef [CellRef "A7"], def { _dvAllowBlank = False , _dvError = Just "aaa" , _dvErrorStyle = ErrorStyleStop , _dvErrorTitle = Just "bbb" , _dvPrompt = Just "ccc" , _dvPromptTitle = Just "ddd" , _dvShowDropDown = False , _dvShowErrorMessage = False , _dvShowInputMessage = False , _dvValidationType = ValidationTypeWhole $ ValNotBetween (Formula "10") (Formula "12") } ) ] testPivotTable :: PivotTable testPivotTable = PivotTable { _pvtName = "PivotTable1" , _pvtDataCaption = "Values" , _pvtLocation = CellRef "A3:D12" , _pvtSrcRef = CellRef "A1:D5" , _pvtSrcSheet = "Sheet1" , _pvtRowFields = [FieldPosition colorField, DataPosition] , _pvtColumnFields = [FieldPosition yearField] , _pvtDataFields = [ DataField { _dfName = "Sum of field Price" , _dfField = priceField , _dfFunction = ConsolidateSum } , DataField { _dfName = "Sum of field Count" , _dfField = countField , _dfFunction = ConsolidateSum } ] , _pvtFields = [ PivotFieldInfo colorField False , PivotFieldInfo yearField True , PivotFieldInfo priceField False , PivotFieldInfo countField False ] , _pvtRowGrandTotals = True , _pvtColumnGrandTotals = False , _pvtOutline = False , _pvtOutlineData = False } where colorField = PivotFieldName "Color" yearField = PivotFieldName "Year" priceField = PivotFieldName "Price" countField = PivotFieldName "Count" testPivotTableDefinition :: ByteString testPivotTableDefinition = [r| |] testPivotCacheDefinition :: ByteString testPivotCacheDefinition = [r| |] stripContentSpaces :: Document -> Document stripContentSpaces doc@Document {documentRoot = root} = doc {documentRoot = go root} where go e@Element {elementNodes = nodes} = e {elementNodes = mapMaybe goNode nodes} goNode (NodeElement el) = Just $ NodeElement (go el) goNode t@(NodeContent txt) = if T.strip txt == T.empty then Nothing else Just t goNode other = Just $ other