-- | Copyright 2013-2015 Anchor Systems, Pty Ltd and Others -- -- The code in this file, and the program it is a part of, is -- made available to you by its authors as open source software: -- you can redistribute it and/or modify it under the terms of -- the 3-clause BSD licence. -- -- /Description/ -- This module defines unit of measurement for metric reports. -- {-# OPTIONS -fno-warn-missing-signatures #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE ViewPatterns #-} module Borel.Types.UOM ( -- * Unit of measurement UOM(..), BaseUOM(..), Prefix(..) -- * Convenient constructors , sec, nanosec, byte, megabyte, gigabyte , countCPU, countVCPU, countInstance, countIP -- * Conversions , convert, tryConvert , nanosecToSec, byteToGigabyte, nanosecToHour -- * Utilities , pUOM, pPrefixUOM, pBaseUOM , flattenUOM, mapUOM ) where import Control.Applicative import Control.Error.Util import Control.Lens (Prism', preview, prism', re, review, (^.), (^?)) import Control.Monad import Data.Aeson import qualified Data.Attoparsec.Text as AT import Data.Csv (FromField, ToField, parseField, toField) import Data.Maybe import Data.Monoid import Data.MultiSet (MultiSet) import qualified Data.MultiSet as S import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as E import Data.Word -------------------------------------------------------------------------------- -- * Our UOM parser is only total for UOMs made up of -- these components. This is because the format we need -- to work with is restrictive. countCPU = UOM Base CPU countVCPU = UOM Base VCPU countInstance = UOM Base Instance countIP = UOM Base IPAddress nanosec = UOM Nano Second sec = UOM Base Second byte = UOM Base Byte megabyte = UOM Mega Byte gigabyte = UOM Giga Byte hour = UOM Base Hour -------------------------------------------------------------------------------- data UOM = UOM Prefix BaseUOM | Times UOM UOM deriving (Eq, Ord) data Prefix = Base | Giga | Nano | Mebi | Mega deriving (Eq, Ord, Enum, Bounded) data BaseUOM = Second | Hour | Byte | Instance | IPAddress | CPU | VCPU deriving (Eq, Ord, Enum, Bounded) pBaseUOM :: Prism' Text BaseUOM pBaseUOM = prism' pretty parse where pretty Second = "s" pretty Hour = "h" pretty Byte = "B" pretty Instance = "instance" pretty IPAddress = "ip" pretty CPU = "cpu" pretty VCPU = "vcpu" parse "s" = Just Second parse "h" = Just Hour parse "B" = Just Byte parse "instance" = Just Instance parse "ip" = Just IPAddress parse "cpu" = Just CPU parse "vcpu" = Just VCPU parse _ = Nothing pPrefixUOM :: Prism' Text Prefix pPrefixUOM = prism' pretty parse where pretty Base = "" pretty Giga = "G" pretty Nano = "n" pretty Mebi = "Mi" pretty Mega = "M" parse "" = Just Base parse "G" = Just Giga parse "n" = Just Nano parse "Mi" = Just Mebi parse "M" = Just Mega parse _ = Nothing pUOM :: Prism' Text UOM pUOM = prism' pretty parse where dash = "-" pretty (u `Times` v) = (u ^. re pUOM) <> dash <> (v ^. re pUOM) pretty (UOM p b) = (p ^. re pPrefixUOM) <> (b ^. re pBaseUOM) -- treat UOM @Times@ as left-associative. parse = hush . AT.parseOnly (parser <* AT.endOfInput) parser = do uoms <- puom `AT.sepBy` AT.string dash case uoms of [] -> mzero -- construct left-associative UOMs (u:us) -> return $ foldl Times u us puom = do pre <- AT.option (Just Base) ppre base <- pbase case (pre, base) of (Just p, Just b) -> return $ UOM p b _ -> mzero ppre = (preview pPrefixUOM <$> AT.string "G") <|> (preview pPrefixUOM <$> AT.string "n") <|> (preview pPrefixUOM <$> AT.string "Mi") <|> (preview pPrefixUOM <$> AT.string "M") pbase = (preview pBaseUOM <$> AT.string "s") <|> (preview pBaseUOM <$> AT.string "h") <|> (preview pBaseUOM <$> AT.string "B") <|> (preview pBaseUOM <$> AT.string "instance") <|> (preview pBaseUOM <$> AT.string "ip" ) <|> (preview pBaseUOM <$> AT.string "cpu") <|> (preview pBaseUOM <$> AT.string "vcpu") instance Show BaseUOM where show = T.unpack . review pBaseUOM instance Show Prefix where show = T.unpack . review pPrefixUOM instance Show UOM where show = T.unpack . review pUOM instance Read UOM where readsPrec _ (T.pack -> x) = maybe [] (pure . (,"")) $ x ^? pUOM instance FromJSON UOM where parseJSON (String t) = maybe mzero return $ t ^? pUOM parseJSON _ = mzero instance ToJSON UOM where toJSON x = String $ x ^. re pUOM instance FromField UOM where parseField (E.decodeUtf8 -> t) = maybe mzero return $ t ^? pUOM instance ToField UOM where toField x = E.encodeUtf8 $ x ^. re pUOM -------------------------------------------------------------------------------- nanosecToSec :: (UOM, Word64) -> (UOM, Word64) nanosecToSec (old, v) = let new = mapUOM f old in (new, tryConvert old new v) where f p b | UOM p b == nanosec = sec | otherwise = UOM p b byteToGigabyte :: (UOM, Word64) -> (UOM, Word64) byteToGigabyte (old, v) = let new = mapUOM f old in (new, tryConvert old new v) where f p b | UOM p b == byte = gigabyte | otherwise = UOM p b nanosecToHour :: (UOM, Word64) -> (UOM, Word64) nanosecToHour (old,v) = let new = mapUOM f old in (new, tryConvert old new v) where f p b | UOM p b == nanosec = hour | otherwise = UOM p b -- Not a functor. mapUOM :: (Prefix -> BaseUOM -> UOM) -> UOM -> UOM mapUOM f (UOM p b) = f p b mapUOM f (Times x y) = Times (mapUOM f x) (mapUOM f y) flattenUOM :: UOM -> [UOM] flattenUOM x@(UOM _ _) = [x] flattenUOM (Times x y) = flattenUOM x ++ flattenUOM y class Weighed a where weigh :: a -> Double instance Weighed Prefix where weigh Base = 1 weigh Giga = 10^^(9 :: Int) weigh Nano = 10^^(-9 :: Int) weigh Mebi = 1024^^(2 :: Int) weigh Mega = 10^^(6 :: Int) instance Weighed BaseUOM where weigh Hour = 60 * 60 weigh _ = 1 instance Weighed UOM where weigh (UOM p b) = weigh p * weigh b weigh (a `Times` b) = weigh a * weigh b data Dimension = CTime | CData | CInstance | CIPAddress | CCPU | CVCPU deriving (Eq, Ord) dimension :: BaseUOM -> Dimension dimension Second = CTime dimension Hour = CTime dimension Byte = CData dimension Instance = CInstance dimension IPAddress = CIPAddress dimension CPU = CCPU dimension VCPU = CVCPU dimensions :: UOM -> MultiSet Dimension dimensions (UOM _ b) = S.singleton $ dimension b dimensions (a `Times` b) = dimensions a <> dimensions b convert :: UOM -> UOM -> Maybe (Word64 -> Word64) convert old new | dimensions old == dimensions new = let factor = weigh old / weigh new in Just (floor . (*) (toRational factor) . toRational) | otherwise = Nothing tryConvert :: UOM -> UOM -> Word64 -> Word64 tryConvert old new = fromMaybe id $ convert old new