packages feed

alfred-margaret-2.1.0.0: tests/Data/Text/Utf8Spec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Text.Utf8Spec where

import Control.Exception (evaluate)
import Control.Monad (forM_)
import Test.Hspec (Spec, anyErrorCall, describe, it, shouldBe, shouldSatisfy, shouldThrow)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck (Gen, choose, forAllShrink, shrink)

import qualified Data.Char as Char

import Data.Text.TestInstances ()

import qualified Data.Text.Utf8 as Utf8

spec :: Spec
spec = do
    describe "Properties of the BMP in UTF-8" $ do

        describe "Char.toLower" $ do

            {-
            it "does not generate common suffixes" $ do
                forM_ bmpCodepoints $ flip shouldSatisfy $ \cp ->
                    let
                        lowerCp = mapCp Char.toLower cp
                    in
                    cp == lowerCp || null (commonSuffix (Utf8.unicode2utf8 cp) (Utf8.unicode2utf8 lowerCp))
            -- Sadly, it "actually does"
            -}

            it "is idempotent" $ do
                forM_ bmpCodepoints $ flip shouldSatisfy $ \cp ->
                    Char.toLower cp == Char.toLower (Char.toLower cp)

    describe "toLowerAscii" $ do

        it "is equivalent to Char.toLower on ASCII" $ do

            forM_ asciiCodepoints $ flip shouldSatisfy $ \cp ->
                Char.toLower cp == Utf8.toLowerAscii cp

    describe "lowerCodePoint" $ do

        prop "is equivalent to Char.toLower on all of Unicode" $ \c ->
            Utf8.lowerCodePoint c `shouldBe` Char.toLower c

    describe "unlowerCodePoint" $ do

      it "should return nothing if it's not a lower case of anything" $ do
        Utf8.unlowerCodePoint 'A' `shouldBe` ""
        Utf8.unlowerCodePoint 'ẞ' `shouldBe` ""

      it "should return itself if it doesn't have any casings" $ do
        Utf8.unlowerCodePoint '1' `shouldBe` "1"

      it "can return multiple values" $ do
        Utf8.unlowerCodePoint 'a' `shouldBe` "aA"
        Utf8.unlowerCodePoint 'ß' `shouldBe` "ẞß"
        Utf8.unlowerCodePoint 'i' `shouldBe` "İiI"

    describe "isCaseInvariant" $ do
      it "holds vacuously for empty texts" $ do
        Utf8.isCaseInvariant "" `shouldBe` True

      it "should be true for characters without upper/lower cases" $ do
        Utf8.isCaseInvariant "." `shouldBe` True
        Utf8.isCaseInvariant ".,;'123" `shouldBe` True
        Utf8.isCaseInvariant "💩" `shouldBe` True

      it "should be false if there are characters with upper/lower cases" $ do
        Utf8.isCaseInvariant "a" `shouldBe` False
        Utf8.isCaseInvariant "A.." `shouldBe` False
        Utf8.isCaseInvariant "ß." `shouldBe` False
        Utf8.isCaseInvariant "ẞ" `shouldBe` False
        Utf8.isCaseInvariant "İ" `shouldBe` False

    describe "dropWhile" $ do

        it "handles a simple example well" $ do
            Utf8.dropWhile (== 'b') "bbba" `shouldBe` "a"

    describe "slicing functions" $ do

        let
            -- | Example shown in section "Slicing Functions" in 'Data.Text.Utf8".
            slicingExample :: Utf8.Text
            slicingExample = Utf8.Text u8data 1 11
                where Utf8.Text u8data _ _ = Utf8.pack "ABCDEFGHIJKLMN"

        it "satisfies the example in Data.Text.Utf8" $ do
            let begin = Utf8.CodeUnitIndex 2
            let length_ = Utf8.CodeUnitIndex 6
            Utf8.unsafeSliceUtf8 begin length_ slicingExample `shouldBe` "DEFGHI"
            Utf8.unsafeCutUtf8 begin length_ slicingExample `shouldBe` ("BC", "JKL")

        prop "unsafeSliceUtf8 and unsafeCutUtf8 are complementary" $
            forAllShrink (arbitrarySlicingIndices slicingExample) shrink $ \ (begin, length_) -> do
                let (prefix, suffix) = Utf8.unsafeCutUtf8 begin length_ slicingExample
                Utf8.concat [prefix, Utf8.unsafeSliceUtf8 begin length_ slicingExample, suffix] `shouldBe` slicingExample

    describe "Basic Text instances" $ do

        prop "Show Text behaves like Show String" $ \ (str :: String) -> do
            show (Utf8.pack str) `shouldBe` show str

        prop "Eq Text behaves like Eq String" $ \ (a :: String) (b :: String) -> do
            Utf8.pack a == Utf8.pack b `shouldBe` a == b

        prop "Ord Text behaves like Ord String" $ \ (a :: String) (b :: String) -> do
            compare (Utf8.pack a) (Utf8.pack b) `shouldBe` compare a b

    describe "skipCodePointsBackwards" $ do
      it "works with ascii" $ do
        Utf8.skipCodePointsBackwards "abcd" 3 0 `shouldBe` 3
        Utf8.skipCodePointsBackwards "abcd" 3 1 `shouldBe` 2
        Utf8.skipCodePointsBackwards "abcd" 3 2 `shouldBe` 1
        Utf8.skipCodePointsBackwards "abcd" 3 3 `shouldBe` 0

      it "moves to start of codepoint if you skip 0" $ do
        Utf8.skipCodePointsBackwards "💩💩" 0 0 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 1 0 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 2 0 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 3 0 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 4 0 `shouldBe` 4
        Utf8.skipCodePointsBackwards "💩💩" 5 0 `shouldBe` 4
        Utf8.skipCodePointsBackwards "💩💩" 6 0 `shouldBe` 4
        Utf8.skipCodePointsBackwards "💩💩" 7 0 `shouldBe` 4

      it "can skip 1 multi-byte codepoint" $ do
        Utf8.skipCodePointsBackwards "💩💩" 4 1 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 5 1 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 6 1 `shouldBe` 0
        Utf8.skipCodePointsBackwards "💩💩" 7 1 `shouldBe` 0

      it "can skip multiple multi-byte codepoint" $ do
        -- a, İ, ẞ and 💩 have byte lengths 1, 2, 3 and 4
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 0 `shouldBe` 15  -- stays at a
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 1 `shouldBe` 13  -- skips to İ
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 2 `shouldBe` 10  -- skips to ẞ
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 3 `shouldBe` 6   -- skips to 💩
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 4 `shouldBe` 3   -- skips to ẞ
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 5 `shouldBe` 1   -- skips to İ
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 15 6 `shouldBe` 0   -- skips to a
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 14 2 `shouldBe` 6  -- from İ to 💩
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 13 2 `shouldBe` 6  -- from İ to 💩
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 10 3 `shouldBe` 1  -- from ẞ to İ
        Utf8.skipCodePointsBackwards "aİẞ💩ẞİa" 9 3 `shouldBe` 0   -- from 💩 to a

      it "throws errors when you read out of bounds" $ do
        evaluate (Utf8.skipCodePointsBackwards "💩💩" 8 0) `shouldThrow` anyErrorCall
        evaluate (Utf8.skipCodePointsBackwards "💩💩" 7 2) `shouldThrow` anyErrorCall



arbitrarySlicingIndices :: Utf8.Text -> Gen (Utf8.CodeUnitIndex, Utf8.CodeUnitIndex)
arbitrarySlicingIndices example = do
    let exampleLength = Utf8.codeUnitIndex $ Utf8.lengthUtf8 example

    begin <- choose (0, exampleLength)
    length_ <- choose (0, exampleLength - begin)

    pure (Utf8.CodeUnitIndex begin, Utf8.CodeUnitIndex length_)

asciiCodepoints :: [Char]
asciiCodepoints = map Char.chr [0..0x7f]

-- | The Basic Multilingual Plane (BMP) contains the Unicode code points
-- 0x0000 through 0xFFFF.
bmpCodepoints :: [Char]
bmpCodepoints = map Char.chr [0..0xffff]

commonSuffix :: Eq a => [a] -> [a] -> [a]
commonSuffix list list' = reverse $ go (reverse list) (reverse list')
    where
        go (x:xs) (y:ys)
            | x == y = x : go xs ys
        go _ _ = []