packages feed

isotope-0.3.3.0: test/Isotope/BaseSpec.hs

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE FlexibleInstances #-}
module Isotope.BaseSpec (spec) where

import Isotope
import Test.Hspec
import Test.QuickCheck
import Control.Monad
import Data.Char
import Data.List

spec :: Spec
spec = do
    describe "ElementSymbol" $ do
      it "should not have more than two characters" $
        ((<= 2) . length . show) <$> elementSymbolList `shouldSatisfy` and
      it "second character of an ElementSymbol should not be upper case" $
        (\x -> length x /= 2 || (isLower . last) x) . show <$>
        elementSymbolList `shouldSatisfy` and

    describe "lookupElement" .
      it "should not contain duplicate elements" $
        lookupElement <$> elementSymbolList `shouldSatisfy` allUnique

    describe "elementName" $ do
      it "should not be an empty string" $
        elementName <$> elementSymbolList `shouldSatisfy` notElem ""
      it "should not start with a capital letter" $
        isLower . head . elementName <$> elementSymbolList `shouldSatisfy` and
      it "element names should not be duplicated" $
        elementName <$> elementSymbolList `shouldSatisfy` allUnique

    describe "atomicNumber" $ do
      it "should be between 1 and 92" $
        (\x -> x >= 1 && x <= 92) . atomicNumber <$>
        elementSymbolList `shouldSatisfy` and
      it "should not have duplicated atomic numbers" $
        atomicNumber <$> elementSymbolList `shouldSatisfy` allUnique

    describe "isotopes" .
      it "should not have duplicate isotopes" $
        isotopes <$> elementSymbolList `shouldSatisfy` allUnique

    describe "mostAbundantIsotope" .
      it "C should be C12 (six protons and six neutrons)" $
        (nucleons . mostAbundantIsotope) C `shouldBe` (6, 6)

    describe "selectIsotope" .
      it "calling fuction with the arguments C and 12 should select C12" $
        nucleons (selectIsotope C 12) `shouldBe` (6, 6)

    describe "monoisotopicMass" .
      it "calling function with C should be 12.0" $
        monoisotopicMass C `shouldBe` MonoisotopicMass 12.0

    describe "nominalMass" .
      it "calling function with C should return 12" $
        nominalMass C `shouldBe` NominalMass 12

    describe "isotopicMasses" .
      it "H should return a list containing 1.007... and 2.014...." $
        isotopicMasses H `shouldBe` IsotopicMass <$> [1.00782503223, 2.01410177812]

    describe "integerMasses" $ do
      it "calling function with H should return [1, 2]" $
         integerMasses H `shouldBe` [1, 2]
      it "proton number should be equal to atomic number" $
        all protonNumEqAtomicNum elementSymbolList

    describe "averageMass" .
      it "calling function with C should return 12.0107" $
        withinTolerance (getAverageMass (averageMass C)) 12.0107 0.0001 `shouldBe` True

    describe "isotopicAbundances" $ do
      it "calling function with C should return [0.9893, 0.0107]" $
        isotopicAbundances C `shouldBe` IsotopicAbundance <$> [0.9893, 0.0107]
      it "sum of isotopic abundances for an element should equal 1" $
        (\sym -> withinTolerance (sum (getIsotopicAbundance <$> isotopicAbundances sym)) 1 0.0001) <$>
        elementSymbolList `shouldSatisfy` and

    describe "renderFormula for ElementalComposition" $ do
      it "[ele|C6H6O|] should be \"C6H6O\"" $
        renderFormula [ele|C6H6O|] `shouldBe` "C6H6O"
      it "[ele|CCl4|] should be \"CCl4\"" $
        renderFormula [ele|CCl4|] `shouldBe` "CCl4"
      it "[ele|H2O4S|] should be \"H2O4S\"" $
        renderFormula [ele|H2O4S|] `shouldBe` "H2O4S"

    describe "renderFormula for MolecularFormula" $ do
      it "[mol|C6H6O|] should be \"C6H6O\"" $
        renderFormula [mol|C6H6O|] `shouldBe` "C6H6O"
      it "[mol|CCl4|] should be \"CCl4\"" $
        renderFormula [mol|CCl4|] `shouldBe` "CCl4"
      it "[mol|H2O4S|] should be \"H2O4S\"" $
        renderFormula [mol|H2O4S|] `shouldBe` "H2O4S"

    describe "renderFormula for CondensedFormula" $ do
      it "[con|C6H6O|] should be \"C6H6O\"" $
        renderFormula [con|C6H6O|] `shouldBe` "C6H6O"
      it "[con|N(CH3)3|] should be \"N(CH3)3\"" $
        renderFormula [con|N(CH3)3|] `shouldBe` "N(CH3)3"
      it "[con|C6H5OH|] should be \"C6H5OH\"" $
        renderFormula [con|C6H5OH|] `shouldBe` "C6H5OH"

    describe "renderFormula for EmpiricalFormula" $ do
      it "[emp|C6H6O|] should be \"C6H6O\"" $
        renderFormula [emp|C6H6O|] `shouldBe` "C6H6O"
      it "[emp|CCl4|] should be \"CCl4\"" $
        renderFormula [emp|CCl4|] `shouldBe` "CCl4"
      it "[emp|H2O4S|] should be \"H2O4S\"" $
        renderFormula [emp|H2O4S|] `shouldBe` "H2O4S"

    describe "ToElementalComposition - ElementalComposition instance" $ do
      it "toElementalComposition" . property $
        \ec -> toElementalComposition ec == (ec :: ElementalComposition)
      it "monoisotopic mass of ethanol" $
        withinTolerance (getMonoisotopicMass (monoisotopicMass [ele|C2H6O|])) 46.04186 0.0001
        `shouldBe` True
      it "average mass of ethanol" $
        withinTolerance (getAverageMass (averageMass [ele|C2H6O|])) 46.06844 0.0001
        `shouldBe` True
      it "nominalMass mass of ethanol" $
        nominalMass [ele|C2H6O|] `shouldBe` NominalMass 46

    describe "mkElementalComposition" $ do
      it "zero is filtered out" $
        mkElementalComposition [(C, 0), (H, 0), (O, 0)] `shouldBe` emptyFormula
      it "should give the correct formula" $
        mkElementalComposition [(C, 2), (H, 6), (O, 1)] `shouldBe` [ele|C2H6O|]

    describe "ToElementalComposition - ElementSymbol instance" .
      it "monoisotopicMass" . property $
        \sym -> monoisotopicMass sym == monoisotopicMass (mkElementalComposition [(sym, 1)])

    describe "Monoid instance for MolecularFormula" $ do
      it "associativity" . property $
          \a b c -> (a |+| b) |+| c == a |+| (b |+| c :: MolecularFormula)
      it "right identity" . property $
          \a -> (a :: MolecularFormula) |+| emptyFormula == a
      it "left identity" . property $
          \a -> (emptyFormula :: MolecularFormula) |+| a == a

    describe "Addition of molecular formulae is commutative" .
      it "commutative" . property $
        \a b -> (a |+| b) == (b |+| a :: MolecularFormula)

    describe "properties of |+|, |*| and |-|)" $ do
      it "a |-| a = emptyFormula" . property $
        \a -> a |-| a == (emptyFormula :: MolecularFormula)
      it "0 |*| a == emptyFormula" . property $
        \a -> a |*| 0 == (emptyFormula :: MolecularFormula)
      it "a |+| a == 2 |*| a" . property $
        \a -> a |+| a == (a :: MolecularFormula) |*| 2

    describe "ToElementalComposition - MolecularFormula instance" $ do
      it "[mol|C2H6O|] should be [ele|C2H6O|]" $
        toElementalComposition [mol|C2H6O|] `shouldBe` [ele|C2H6O|]
      it "empty MolecularFormula should return an empty ElementalComposition" $
        toElementalComposition (emptyFormula :: ElementalComposition) `shouldBe`
        mkElementalComposition []

    describe "Monoid instance for CondensedFormula" $ do
      it "associativity" . property $
          \a b c -> (a `mappend` b) `mappend` c ==
                     a `mappend` (b `mappend` c :: CondensedFormula)
      it "right identity" . property $
          \a -> (a :: CondensedFormula) `mappend` mempty == a
      it "left identity" . property $
          \a -> emptyFormula `mappend` (a :: CondensedFormula) == a

    describe "ToElementalComposition - CondensedFormula instance" $ do
      it "empty CondensedFormula should return an empty ElementalComposition" $
        toElementalComposition (emptyFormula :: CondensedFormula) `shouldBe`
        [ele||]
      it "[con|N(CH3)3|] should be [ele|C3NH9|]" $
        toElementalComposition [con|N(CH3)3|] `shouldBe` [ele|C3NH9|]

    describe "ToMolecularFormula - CondensedFormula instance" $ do
      it "empty CondensedFormula should return an empty MolecularFormula" $
        toMolecularFormula (emptyFormula :: CondensedFormula) `shouldBe`
        mkMolecularFormula []
      it "[mol|C6H6|] should be [emp|CH|]" $
        toMolecularFormula [con|N(CH3)3|] `shouldBe` [mol|C3NH9|]

    describe "mkEmpiricalFormula" $ do
      it "zero is filtered out" $
        mkEmpiricalFormula [(C, 0), (H, 0), (O, 0)] `shouldBe` emptyFormula
      it "should give the correct formula" $
        mkEmpiricalFormula [(C, 6), (H, 6)] `shouldBe` [emp|CH|]

    describe "ToEmpiricalFormula - ElementalComposition instance" $ do
      it "empty ElementalComposition should return an empty EmpiricalFormula" $
        toEmpiricalFormula (emptyFormula :: ElementalComposition) `shouldBe`
        mkEmpiricalFormula []
      it "[ele|C6H6|] should be [emp|CH|]" $
        toEmpiricalFormula [ele|C6H6|] `shouldBe` [emp|CH|]

    describe "ToEmpiricalFormula - MolecularFormula instance" $ do
      it "empty MolecularFormula should return an empty EmpiricalFormula" $
        toEmpiricalFormula (emptyFormula :: MolecularFormula) `shouldBe`
        mkEmpiricalFormula []
      it "[mol|C6H6|] should be [emp|CH|]" $
        toEmpiricalFormula [mol|C6H6|] `shouldBe` [emp|CH|]

    describe "ToEmpiricalFormula - CondensedFormula instance" $ do
      it "empty CondensedFormula should return an empty EmpiricalFormula" $
        toEmpiricalFormula (emptyFormula :: CondensedFormula) `shouldBe`
        mkEmpiricalFormula []
      it "[con|C6H6|] should be [emp|CH|]" $
        toEmpiricalFormula [con|C6H6|] `shouldBe` [emp|CH|]

    describe "ToElementalComposition - EmpiricalFormula instance" $ do
      it "empty EmpiricalFormula should return an empty EmpiricalFormula" $
        toElementalComposition (emptyFormula :: EmpiricalFormula) `shouldBe`
        mkElementalComposition []
      it "[emp|CH|] should be [ele|CH|]" $
        toElementalComposition [emp|CH|] `shouldBe` [ele|CH|]

    describe "Laws for ElementalComposition, MolecularFormula, EmpiricalFormula and CondensedFormula data types" $ do
      it "applying toEmpiricalFormula to a CondensedFormula should give the same result as applying toMolecularFormula compose toEmpiricalFormula" . property $
        \c -> toEmpiricalFormula c == (toEmpiricalFormula . toMolecularFormula) (c :: CondensedFormula)
      it "applying toElementalComposition to a CondensedFormula should give the same result as applying toMolecularFormula compose toElementalComposition" . property $
        \c -> toElementalComposition c == (toElementalComposition . toMolecularFormula) (c :: CondensedFormula)
      it "applying toElementalComposition compose toEmpiricalFormula to an EmpiricalFormula should return the same EmpiricalFormula" . property $
        \e -> (toEmpiricalFormula . toElementalComposition) e == (e :: EmpiricalFormula)

allUnique :: (Eq a) => [a] -> Bool
allUnique l = l == nub l

withinTolerance :: (Num a, Ord a) => a -> a -> a -> Bool
withinTolerance n1 n2 err = abs (n1 - n2) < err

protonNumEqAtomicNum :: ElementSymbol -> Bool
protonNumEqAtomicNum sym =
  and $ (== atomicNumber sym) . fst . nucleons <$> isotopes sym

elemSymIntPairGen :: Gen (ElementSymbol, Int)
elemSymIntPairGen = do
    elemSym <- arbitrary
    n <- choose (1,100)
    return (elemSym, n)

elemSymIntPairListGen :: Gen [(ElementSymbol, Int)]
elemSymIntPairListGen = listOf elemSymIntPairGen

instance Arbitrary ElementSymbol where
    arbitrary = oneof $ return <$> elementSymbolList

instance Arbitrary ElementalComposition where
  arbitrary = mkElementalComposition <$> elemSymIntPairListGen

instance Arbitrary MolecularFormula where
  arbitrary = mkMolecularFormula <$> elemSymIntPairListGen

instance Arbitrary CondensedFormula where
  arbitrary = do
    n <- choose (0, 4)
    condForm <- vectorOf n
      (oneof [leftCondensedFormulaGen, sized arbRightCondensedFormulaGen])
    return $ CondensedFormula condForm
    where
      leftCondensedFormulaGen :: Gen (Either MolecularFormula (CondensedFormula, Int))
      leftCondensedFormulaGen = Left <$> arbitrary
      arbRightCondensedFormulaGen :: Int -> Gen (Either MolecularFormula (CondensedFormula, Int))
      arbRightCondensedFormulaGen 0 = do
        m <- choose (1, 4)
        condForm <- leftCondensedFormulaGen
        return $ Right (CondensedFormula [condForm], m)
      arbRightCondensedFormulaGen n | n > 0 = do
        m <- choose (1, 4)
        v <- choose (1, 3)
        let n' = n `div` (v + 1)
        condForm' <- replicateM v (arbRightCondensedFormulaGen n')
        return $ Right (CondensedFormula condForm', m)

instance Arbitrary EmpiricalFormula where
  arbitrary = mkEmpiricalFormula <$> elemSymIntPairListGen