module Codec.Xlsx.Formatted (
FormattedCell(..)
, Formatted(..)
, formatted
, toFormattedCells
, CondFormatted(..)
, conditionallyFormatted
, formatAlignment
, formatBorder
, formatFill
, formatFont
, formatNumberFormat
, formatProtection
, formatPivotButton
, formatQuotePrefix
, formattedCell
, formattedFormat
, formattedColSpan
, formattedRowSpan
, condfmtCondition
, condfmtDxf
, condfmtPriority
, condfmtStopIfTrue
) where
import Control.Lens
import Control.Monad.State hiding (forM_, mapM)
import Data.Default
import Data.Foldable (asum, forM_)
import Data.Function (on)
import Data.List (foldl', groupBy, sortBy, sortBy)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Traversable (mapM)
import Data.Tuple (swap)
import Prelude hiding (mapM)
import Safe (headNote)
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
import Codec.Xlsx.Types
data FormattingState = FormattingState {
_formattingBorders :: Map Border Int
, _formattingCellXfs :: Map CellXf Int
, _formattingFills :: Map Fill Int
, _formattingFonts :: Map Font Int
, _formattingNumFmts :: Map Text Int
, _formattingMerges :: [Range]
}
makeLenses ''FormattingState
stateFromStyleSheet :: StyleSheet -> FormattingState
stateFromStyleSheet StyleSheet{..} = FormattingState{
_formattingBorders = fromValueList _styleSheetBorders
, _formattingCellXfs = fromValueList _styleSheetCellXfs
, _formattingFills = fromValueList _styleSheetFills
, _formattingFonts = fromValueList _styleSheetFonts
, _formattingNumFmts = M.empty
, _formattingMerges = []
}
fromValueList :: Ord a => [a] -> Map a Int
fromValueList = M.fromList . (`zip` [0..])
toValueList :: Map a Int -> [a]
toValueList = map snd . sortBy (comparing fst) . map swap . M.toList
updateStyleSheetFromState :: StyleSheet -> FormattingState -> StyleSheet
updateStyleSheetFromState sSheet FormattingState{..} = sSheet
{ _styleSheetBorders = toValueList _formattingBorders
, _styleSheetCellXfs = toValueList _formattingCellXfs
, _styleSheetFills = toValueList _formattingFills
, _styleSheetFonts = toValueList _formattingFonts
, _styleSheetNumFmts = M.fromList . map swap $ M.toList _formattingNumFmts
}
getId :: Ord a => Lens' FormattingState (Map a Int) -> a -> State FormattingState Int
getId = getId' 0
getId' :: Ord a
=> Int
-> Lens' FormattingState (Map a Int)
-> a
-> State FormattingState Int
getId' k f v = do
aMap <- use f
case M.lookup v aMap of
Just anId -> return anId
Nothing -> do let anId = k + M.size aMap
f %= M.insert v anId
return anId
data FormattedCondFmt = FormattedCondFmt
{ _condfmtCondition :: Condition
, _condfmtDxf :: Dxf
, _condfmtPriority :: Int
, _condfmtStopIfTrue :: Maybe Bool
} deriving (Eq, Show)
makeLenses ''FormattedCondFmt
data Format = Format
{ _formatAlignment :: Maybe Alignment
, _formatBorder :: Maybe Border
, _formatFill :: Maybe Fill
, _formatFont :: Maybe Font
, _formatNumberFormat :: Maybe NumberFormat
, _formatProtection :: Maybe Protection
, _formatPivotButton :: Maybe Bool
, _formatQuotePrefix :: Maybe Bool
} deriving (Eq, Show)
makeLenses ''Format
data FormattedCell = FormattedCell
{ _formattedCell :: Cell
, _formattedFormat :: Format
, _formattedColSpan :: Int
, _formattedRowSpan :: Int
} deriving (Eq, Show)
makeLenses ''FormattedCell
instance Default FormattedCell where
def = FormattedCell
{ _formattedCell = def
, _formattedFormat = def
, _formattedColSpan = 1
, _formattedRowSpan = 1
}
instance Default Format where
def = Format
{ _formatAlignment = Nothing
, _formatBorder = Nothing
, _formatFill = Nothing
, _formatFont = Nothing
, _formatNumberFormat = Nothing
, _formatProtection = Nothing
, _formatPivotButton = Nothing
, _formatQuotePrefix = Nothing
}
instance Default FormattedCondFmt where
def = FormattedCondFmt ContainsBlanks def topCfPriority Nothing
data Formatted = Formatted {
formattedCellMap :: CellMap
, formattedStyleSheet :: StyleSheet
, formattedMerges :: [Range]
} deriving (Eq, Show)
formatted :: Map (Int, Int) FormattedCell -> StyleSheet -> Formatted
formatted cs styleSheet =
let initSt = stateFromStyleSheet styleSheet
(cs', finalSt) = runState (mapM (uncurry formatCell) (M.toList cs)) initSt
styleSheet' = updateStyleSheetFromState styleSheet finalSt
in Formatted {
formattedCellMap = M.fromList (concat cs')
, formattedStyleSheet = styleSheet'
, formattedMerges = reverse (finalSt ^. formattingMerges)
}
toFormattedCells :: CellMap -> [Range] -> StyleSheet -> Map (Int, Int) FormattedCell
toFormattedCells m merges StyleSheet{..} = applyMerges $ M.map toFormattedCell m
where
toFormattedCell cell@Cell{..} =
FormattedCell
{ _formattedCell = cell{ _cellStyle = Nothing }
, _formattedFormat = maybe def formatFromStyle $ flip M.lookup cellXfs =<< _cellStyle
, _formattedColSpan = 1
, _formattedRowSpan = 1 }
formatFromStyle cellXf =
Format
{ _formatAlignment = applied _cellXfApplyAlignment _cellXfAlignment cellXf
, _formatBorder = flip M.lookup borders =<<
applied _cellXfApplyBorder _cellXfBorderId cellXf
, _formatFill = flip M.lookup fills =<<
applied _cellXfApplyFill _cellXfFillId cellXf
, _formatFont = flip M.lookup fonts =<<
applied _cellXfApplyFont _cellXfFontId cellXf
, _formatNumberFormat = lookupNumFmt =<<
applied _cellXfApplyNumberFormat _cellXfNumFmtId cellXf
, _formatProtection = _cellXfProtection cellXf
, _formatPivotButton = _cellXfPivotButton cellXf
, _formatQuotePrefix = _cellXfQuotePrefix cellXf }
idMapped :: [a] -> Map Int a
idMapped = M.fromList . zip [0..]
cellXfs = idMapped _styleSheetCellXfs
borders = idMapped _styleSheetBorders
fills = idMapped _styleSheetFills
fonts = idMapped _styleSheetFonts
lookupNumFmt fId = asum
[ StdNumberFormat <$> idToStdNumberFormat fId
, UserNumberFormat <$> M.lookup fId _styleSheetNumFmts]
applied :: (CellXf -> Maybe Bool) -> (CellXf -> Maybe a) -> CellXf -> Maybe a
applied applyProp prop cXf = do
apply <- applyProp cXf
if apply then prop cXf else fail "not applied"
applyMerges cells = foldl' onlyTopLeft cells merges
onlyTopLeft cells range = flip execState cells $ do
let ((r1, c1), (r2, c2)) = fromRange range
nonTopLeft = tail [(r, c) | r<-[r1..r2], c<-[c1..c2]]
forM_ nonTopLeft (modify . M.delete)
at (r1, c1) . non def . formattedRowSpan .= (r2 r1 +1)
at (r1, c1) . non def . formattedColSpan .= (c2 c1 +1)
data CondFormatted = CondFormatted {
condformattedStyleSheet :: StyleSheet
, condformattedFormattings :: Map SqRef ConditionalFormatting
} deriving (Eq, Show)
conditionallyFormatted :: Map CellRef [FormattedCondFmt] -> StyleSheet -> CondFormatted
conditionallyFormatted cfs styleSheet = CondFormatted
{ condformattedStyleSheet = styleSheet & styleSheetDxfs .~ finalDxfs
, condformattedFormattings = fmts
}
where
(cellFmts, dxf2id) = runState (mapM (mapM mapDxf) cfs) dxf2id0
dxf2id0 = fromValueList (styleSheet ^. styleSheetDxfs)
fmts = M.fromList . map mergeSqRef . groupBy ((==) `on` snd) .
sortBy (comparing snd) $ M.toList cellFmts
mergeSqRef cellRefs2fmt =
(SqRef (map fst cellRefs2fmt),
headNote "fmt group should not be empty" (map snd cellRefs2fmt))
finalDxfs = toValueList dxf2id
formatCell :: (Int, Int) -> FormattedCell -> State FormattingState [((Int, Int), Cell)]
formatCell (row, col) cell = do
let (block, mMerge) = cellBlock (row, col) cell
forM_ mMerge $ \merge -> formattingMerges %= (:) merge
mapM go block
where
go :: ((Int, Int), FormattedCell) -> State FormattingState ((Int, Int), Cell)
go (pos, c@ FormattedCell{..}) = do
styleId <- cellStyleId c
return (pos, _formattedCell{_cellStyle = styleId})
cellBlock :: (Int, Int) -> FormattedCell
-> ([((Int, Int), FormattedCell)], Maybe Range)
cellBlock (row, col) cell@FormattedCell{..} = (block, merge)
where
block :: [((Int, Int), FormattedCell)]
block = [ ((row', col'), cellAt (row', col'))
| row' <- [topRow .. bottomRow]
, col' <- [leftCol .. rightCol]
]
merge :: Maybe Range
merge = do guard (topRow /= bottomRow || leftCol /= rightCol)
return $ mkRange (topRow, leftCol) (bottomRow, rightCol)
cellAt :: (Int, Int) -> FormattedCell
cellAt (row', col') =
if row' == row && col == col'
then cell
else def & formattedFormat . formatBorder ?~ borderAt (row', col')
border = _formatBorder _formattedFormat
borderAt :: (Int, Int) -> Border
borderAt (row', col') = def
& borderTop .~ do guard (row' == topRow) ; _borderTop =<< border
& borderBottom .~ do guard (row' == bottomRow) ; _borderBottom =<< border
& borderLeft .~ do guard (col' == leftCol) ; _borderLeft =<< border
& borderRight .~ do guard (col' == rightCol) ; _borderRight =<< border
topRow, bottomRow, leftCol, rightCol :: Int
topRow = row
bottomRow = row + _formattedRowSpan 1
leftCol = col
rightCol = col + _formattedColSpan 1
cellStyleId :: FormattedCell -> State FormattingState (Maybe Int)
cellStyleId c = mapM (getId formattingCellXfs) =<< constructCellXf c
constructCellXf :: FormattedCell -> State FormattingState (Maybe CellXf)
constructCellXf FormattedCell{_formattedFormat=Format{..}} = do
mBorderId <- getId formattingBorders `mapM` _formatBorder
mFillId <- getId formattingFills `mapM` _formatFill
mFontId <- getId formattingFonts `mapM` _formatFont
let getFmtId :: Lens' FormattingState (Map Text Int) -> NumberFormat -> State FormattingState Int
getFmtId _ (StdNumberFormat fmt) = return (stdNumberFormatId fmt)
getFmtId l (UserNumberFormat fmt) = getId' firstUserNumFmtId l fmt
mNumFmtId <- getFmtId formattingNumFmts `mapM` _formatNumberFormat
let xf = CellXf {
_cellXfApplyAlignment = apply _formatAlignment
, _cellXfApplyBorder = apply mBorderId
, _cellXfApplyFill = apply mFillId
, _cellXfApplyFont = apply mFontId
, _cellXfApplyNumberFormat = apply _formatNumberFormat
, _cellXfApplyProtection = apply _formatProtection
, _cellXfBorderId = mBorderId
, _cellXfFillId = mFillId
, _cellXfFontId = mFontId
, _cellXfNumFmtId = mNumFmtId
, _cellXfPivotButton = _formatPivotButton
, _cellXfQuotePrefix = _formatQuotePrefix
, _cellXfId = Nothing
, _cellXfAlignment = _formatAlignment
, _cellXfProtection = _formatProtection
}
return $ if xf == def then Nothing else Just xf
where
apply :: Maybe a -> Maybe Bool
apply Nothing = Nothing
apply (Just _) = Just True
mapDxf :: FormattedCondFmt -> State (Map Dxf Int) CfRule
mapDxf FormattedCondFmt{..} = do
dxf2id <- get
dxfId <- case M.lookup _condfmtDxf dxf2id of
Just i ->
return i
Nothing -> do
let newId = M.size dxf2id
modify $ M.insert _condfmtDxf newId
return newId
return CfRule
{ _cfrCondition = _condfmtCondition
, _cfrDxfId = Just dxfId
, _cfrPriority = _condfmtPriority
, _cfrStopIfTrue = _condfmtStopIfTrue
}