-- El uso Unsafe viene de import Data.Matrix. Ese módulo solo se usa -- para hacer pretty printing. {-# LANGUAGE Safe #-} -- | Módulo general para hojas de cálculo de la herramienta @haskcell@. module Data.SpreadSheet ( SpreadSheet(..) -- Para reescribir en Data.SpreadSheet.Internal , Pos -- , Pos'(..) , Range(..) -- * Constructores , empty , singleton , singletonPos , fromList , toListValues , toListPosValues , fromDict -- * Selección y referencias , get , selectRange , row , column , select -- * Modificación , put , union , intersection , (</>) , mapMaybe -- * Propiedades derivadas , width , height , limits , columns , rows ) where import qualified Data.Map.Strict as Map import Data.Foldable (toList) import Data.Bifunctor (bimap) import Data.Tuple (swap) import Data.Ix (Ix(..)) import Numeric.Natural (Natural(..)) -- | Posición de una celda en la hoja de cálculo. Comienzan en @(1,1)@. -- -- Punto bidimensional en el que la primera coordenada corresponde a -- la columna y la segunda a la fila. -- -- Ejemplo para localizar una celda: -- -- +------+----------+ -- | @A3@ | @(1,3)@ | -- +------+----------+ -- | @J9@ | @(10,9)@ | -- +------+----------+ type Pos = (Natural, Natural) -- data Pos' = Pos' -- ( Int -- ^ Columna -- , Int -- ^ Fila -- ) deriving (Show, Eq, Ord, Ix) -- | Un rango está definido por dos posiciones, la superior izquierda -- y la inferior derecha. -- -- Es utilizado en la generación de rangos con la función 'range'. data Range = Range Pos Pos deriving (Show, Eq, Ord, Ix) -- | 'SpreadSheet' representa la estructura de datos de una hoja de -- cálculo. -- -- Las hojas cálculo pueden contener cualquier tipo de dato, pero solo -- valores de dicho tipo. Es decir, no se podrán tener hojas de -- cálculo heterogéneas que contengan valores 'Double' o 'Bool' a la -- vez. Para una representación más cercana al uso ordinario de las -- hojas de cálculo se recomienda mirar el módulo -- 'Data.SpreadSheet.Cell.Cell'. -- -- La implementación interna de esta estructura de datos es mediante -- un diccionario ('Data.Map.Map' 'Data.Map.Strict') donde las claves son posiciones ('Pos'). -- -- En este módulo se definen un conjunto de operaciones para la -- creación, consulta y modificación de hojas de cálculo. data SpreadSheet a = Mk { topl :: Pos -- Top Left Position , botr :: Pos -- Bottom Right Position , mp :: Map.Map Pos a } | Empty -- | La igualdad entre dos hojas de cálculo se produce cuando el valor -- de todas sus celdas son iguales y cuando hacen referencia al mismo -- rango de posiciones. instance Eq a => Eq (SpreadSheet a) where Empty == Empty = True (Mk tl1 br1 s1) == (Mk tl2 br2 s2) = (tl1 == tl2) && (br1 == br2) && (s1 == s2) _ == _ = False instance Functor SpreadSheet where fmap f Empty = Empty fmap f (Mk tl br s) = Mk tl br (Map.map f s) -- | Operación binaria asociativa implementada con 'union'. instance Semigroup (SpreadSheet a) where (<>) = union instance Monoid (SpreadSheet a) where mempty = Empty instance Foldable SpreadSheet where foldr _ z Empty = z foldr f z (Mk _ _ s) = Map.foldr f z s toList = toListValues -- | Consultar -- 'Data.SpreadSheet.Internal.Pretty.prettyShowSpreadSheet' para una -- visualización más avanzada. instance {-# OVERLAPPABLE #-} Show a => Show (SpreadSheet a) where show = showSpreadSheet showSpreadSheet :: Show a => SpreadSheet a -> String showSpreadSheet Empty = "Empty" showSpreadSheet (Mk tl br s) = show (Range tl br) ++ "\n" ++ (show s) {- Propiedades derivadas -} -- | Indica el número de columnas que tiene una hoja de cálculo. width :: SpreadSheet a -> Maybe Natural width Empty = Nothing width (Mk (x1, _) (x2, _) _) = Just (x2 - x1) -- | Indica el número de filas que tiene una hoja de cálculo. height :: SpreadSheet a -> Maybe Natural height Empty = Nothing height (Mk (_, y1) (_, y2) _) = Just (y2 - y1) -- | Indica el rango en el que están contenidos los valores de la hoja -- de cálculo. limits :: SpreadSheet a -> Maybe Range limits Empty = Nothing limits (Mk tl br _) = Just $ Range tl br -- | Enumera las columnas de la hoja de cálculo. -- -- >>> columns empty -- [] -- -- >>> columns $ fromList [((5,3), True), ((10,6), True)] -- [5,6,7,8,9,10] columns :: SpreadSheet a -> [Natural] columns Empty = [] columns (Mk (tc, _) (bc, _) _) = [tc..bc] -- | Enumera las filas de la hoja de cálculo. -- -- >>> rows empty -- [] -- -- >>> rows $ fromList [((5,3), True), ((10,6), True)] -- [3,4,5,6] rows :: SpreadSheet a -> [Natural] rows Empty = [] rows (Mk (_, tr) (_, br) _) = [tr..br] {- Constructures -} -- | Hoja de cálculo vacía. -- -- >>> empty == fromList [] -- True empty :: SpreadSheet a empty = Empty -- | A partir de un solo valor, se genera la hoja que en la posición -- (1,1) tiene dicho valor. -- -- >>> singleton True -- Range (1,1) (1,1) -- fromList [((1,1), True)] -- singleton :: a -> SpreadSheet a singleton = singletonPos (1,1) -- | Generar una hoja con un valor en una posición. -- -- >>> singletonPos (1,5) True -- Range (1,5) (1,5) -- fromList [((1,5),True)] -- singletonPos :: Pos -> a -> SpreadSheet a singletonPos p v = fromList [(p,v)] -- | De una lista de pares posición-valor se genera la hoja -- correspondiente. En el caso de tener posiciones repetidas, -- permanecerá la que tenga un índice mayor. -- -- >>> fromList [((1,1), 0), ((1,2), 1), ((1,1), 5)] -- Range (1,1) (1,2) -- fromList [((1,1),5),((1,2),3)] -- fromList :: [(Pos, a)] -> SpreadSheet a fromList = fromDict . Map.fromList -- | Construcción de forma equivalente a 'fromList', salvo que se usa un -- diccionario de la librearía 'Data.Map.Map'. -- -- Destinado a construir la hoja de cálculo aprovechando las funciones -- optimizadas para diccionarios. Las precondiciones de las funciones -- de construcción de diccionarios no se comprueban, ya que forman -- parte de otro módulo. -- -- >>> let cells = zipWith (,) [(x,y) | x <- [1..9], y <- [1..9]] [1..] -- >>> Map.valid $ Map.fromAscList cells -- True -- >>> Map.valid $ Map.fromAscList (reverse cells) -- False -- >>> fromDict $ Map.fromAscList cells -- Range (1,1) (9,9) -- fromList [((1,1),1),((1,2),2),((1,3),3),((1,4),... fromDict :: Map.Map Pos a -> SpreadSheet a fromDict s | Map.null s = Empty | otherwise = fromDict' init end s where init = bimap min (min . (Map.mapKeys swap)) (s,s) end = bimap max (max . (Map.mapKeys swap)) (s,s) min = fst . fst . Map.findMin max = fst . fst . Map.findMax fromDict' :: Pos -> Pos -> Map.Map Pos a -> SpreadSheet a fromDict' init end s = Mk init end s -- | Transforma una hoja de cálculo a una lista que contiene sus -- valores. -- -- >>> toListValues empty == [] -- True -- -- La función 'Data.Foldable.toList' aplicada a esta esctructura de -- datos utiliza esta función. toListValues :: SpreadSheet a -> [a] toListValues Empty = [] toListValues (Mk _ _ s) = map snd $ Map.toList s -- | Transforma una hoja de cálculo a una lista que contiene sus -- valores junto a las posiciones en las que están. toListPosValues :: SpreadSheet a -> [(Pos, a)] toListPosValues Empty = [] toListPosValues (Mk _ _ s) = Map.toList s {- Modificación -} -- Probablemente se añada tambiém para hacer a <> a como resultado -- | Combina dos hojas de cálculo. En el caso de que dos celdas -- contengan datos en cada una de las dos hojas, dejará el dato de la -- segunda. -- -- También se puede usar el operador @(<>)@ de la clase 'Semigroup'. union :: SpreadSheet a -> SpreadSheet a -> SpreadSheet a union Empty x = x union x Empty = x union (Mk tl1 br1 s1) (Mk tl2 br2 s2) = Mk tl br s where tl = (min (fst tl1) (fst tl2), min (snd tl1) (snd tl2)) br = (max (fst br1) (fst br2), max (snd br1) (snd br2)) s = flip Map.union s1 s2 -- | Genera una hoja de cálculo nueva con la intersección de las -- celdas, dejando los valores de la primera hoja. intersection :: SpreadSheet a -> SpreadSheet a -> SpreadSheet a intersection Empty _ = Empty intersection _ Empty = Empty intersection s s' = fromDict $ Map.intersection (mp s) (mp s') -- | Operador para la intersección. Ver 'intersection'. (</>) :: SpreadSheet a -> SpreadSheet a -> SpreadSheet a (</>) = intersection -- | Inserta en una posición un valor. Este valor se puede generar -- según una función sobre una hoja de cálculo. -- -- Esta función permite la composición de sucesivas inserciones. -- -- >>> empty & put (1,1) (const True) & put (1,2) (const False) -- Range (1,1) (1,2) -- fromList [((1,1),True),((1,2),False)] put :: Pos -> (SpreadSheet a -> a) -> SpreadSheet a -> SpreadSheet a put pos f Empty = fromDict $ Map.singleton pos (f Empty) put pos f s@(Mk tl tr m) = fromDict $ Map.insert pos (f s) m {- Seleccionar un rango de una SpreadSheet Permite definir rangos fijos, como por ejemplo: a3a4 = range Range (1,3) (1,4) :: Spreadsheet -> Spreadsheet -- -} -- | Obtiene el valor de una posición ('Pos') de una hoja de cálculo. -- -- Si la celda contiene un dato, devuelve el valor como ('Just' @valor@). Si no existe, devuelve 'Nothing'. -- -- >>> get (1,1) $ fromList [((1,1), "valor")] -- Just "valor" -- -- >>> get (2,2) $ fromList [((1,1), "valor")] -- Nothing -- get :: Pos -> SpreadSheet a -> Maybe a get _ Empty = Nothing get p (Mk _ _ s) = Map.lookup p s -- | Genera una hoja de cálculo a partir de una dada según una función -- discriminante sobre las posiciónes. -- -- En el siguiente ejemplo se eligen por ejemplo las celdas presentes en la diagonal. -- -- >>> select (\(col,row) -> col == row) $ fromList [((1,1), "a"), ((2,1), "b"), ((1,2), "c"), ((2,2), "d")] -- Range (1,1) (2,2) -- fromList [((1,1),"a"),((2,2),"d")] select :: (Pos -> Bool) -> SpreadSheet a -> SpreadSheet a select _ Empty = Empty select f s = fromDict . fst . (Map.partitionWithKey (\k _ -> f k)) $ mp s -- | Dando un rango y una hoja de cálculo, genera una hoja de cálculo -- con las celdas en dicho rango. -- -- >>> let cells = zipWith (,) [(x,y) | x <- [1..9], y <- [1..9]] [1..] -- >>> range (Range (8,7) (9,9)) $ fromList cells -- Range (8,7) (9,9) -- fromList [((8,7),70),((8,8),71),((8,9),72),((9,7),79),((9,8),80),((9,9),81)] selectRange :: Range -> SpreadSheet a -> SpreadSheet a selectRange (Range (ic, ir) (fc, fr)) s | ic >= 0 && ir >= 0 && ic <= fc && ir <= fr = go | otherwise = Empty where go = select (\(c,r) -> ic <= c && ir <= r && fc >= c && fr >= r) s -- | Selecciona las celdas con datos de una hoja de cálculo para una -- columna dada. -- -- >>> let cells = zipWith (,) [(x,y) | x <- [1..5], y <- [1..5]] [1..] -- >>> column 2 $ fromList cells -- Range (2,1) (2,5) -- fromList [((2,1),6),((2,2),7),((2,3),8),((2,4),9),((2,5),10)] column :: Natural -> SpreadSheet a -> SpreadSheet a column n = select (\(c,r) -> c == n) -- | Selecciona las celdas con datos de una hoja de cálculo para una -- fila dada. -- -- >>> let cells = zipWith (,) [(x,y) | x <- [1..5], y <- [1..5]] [1..] -- >>> row 3 $ fromList cells -- Range (1,3) (5,3) -- fromList [((1,3),3),((2,3),8),((3,3),13),((4,3),18),((5,3),23)] row :: Natural -> SpreadSheet a -> SpreadSheet a row n = select (\(c,r) -> r == n) -- -- | La función 'mapMaybe' está orientada a la selección de valores de -- un tipo que puedan ser transformados a otro. Tomando una hoja de -- cálculo se obtiene una nueva, con un tipo distinto, con solo -- aquellos valores que han podido ser extraidos. -- -- Esta función tiene su ejemplo en el conjunto de funciónes -- 'Data.SpreadSheet.Cell.extractDouble', -- 'Data.SpreadSheet.Cell.extractString' o -- 'Data.SpreadSheet.Cell.extractDay' del módulo 'Data.SpreadSheet.Cell.Cell'. -- -- >>> let days = fromList [((1,1), "2018-12-23"), ((1,2),"-"), ((1,3),"2018-12-25")] :: SpreadSheet String -- >>> let parseDay = (\x -> parseTimeM True defaultTimeLocale "%Y-%-m-%-d" x) :: String -> Maybe Day -- >>> :t mapMaybe parseDay days -- extract p days :: SpreadSheet Day -- >>> mapMaybe parseDay days -- Range (1,1) (1,3) -- fromList [((1,1),2018-12-23),((1,3),2018-12-25)] mapMaybe :: (a -> Maybe b) -> SpreadSheet a -> SpreadSheet b mapMaybe f Empty = Empty mapMaybe f s = fromDict (Map.mapMaybe f (mp s)) -- zipWith :: (a -> b -> c) -> SpreadSheet a -> SpreadSheet b -> SpreadSheet c