packages feed

phino-0.0.0.56: test/RegexpSpec.hs

-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT

{- | Tests for the Regexp module that provides regular expression
matching and replacement using PCRE.
-}
module RegexpSpec where

import Data.ByteString.Char8 qualified as B
import Regexp qualified as R
import Test.Hspec (Spec, anyException, describe, it, shouldBe, shouldReturn, shouldThrow)

spec :: Spec
spec = do
  describe "compile" $ do
    it "compiles a valid pattern" $ do
      _ <- R.compile (B.pack "foo")
      matched <- R.match (B.pack "foo") (B.pack "foobar")
      matched `shouldBe` True

    it "throws on invalid pattern" $
      R.compile (B.pack "[invalid") `shouldThrow` anyException

    it "compiles pattern with groups" $ do
      _ <- R.compile (B.pack "(a)(b)(c)")
      matched <- R.match (B.pack "(a)(b)(c)") (B.pack "abc")
      matched `shouldBe` True

    it "compiles pattern with unicode" $ do
      _ <- R.compile (B.pack "кирилиця")
      matched <- R.match (B.pack "кирилиця") (B.pack "текст кирилиця тут")
      matched `shouldBe` True

    it "compiles empty pattern" $ do
      _ <- R.compile B.empty
      matched <- R.match B.empty (B.pack "anything")
      matched `shouldBe` True

  describe "match" $ do
    it "returns true when pattern matches" $
      R.match (B.pack "hello") (B.pack "hello world") `shouldReturn` True

    it "returns false when pattern does not match" $
      R.match (B.pack "goodbye") (B.pack "hello world") `shouldReturn` False

    it "returns true for partial match" $
      R.match (B.pack "wor") (B.pack "hello world") `shouldReturn` True

    it "returns true for match at start" $
      R.match (B.pack "^hello") (B.pack "hello world") `shouldReturn` True

    it "returns false for anchored pattern not at start" $
      R.match (B.pack "^world") (B.pack "hello world") `shouldReturn` False

    it "returns true for match at end" $
      R.match (B.pack "world$") (B.pack "hello world") `shouldReturn` True

    it "returns true with empty input and empty pattern" $
      R.match B.empty B.empty `shouldReturn` True

    it "returns true with non-empty input and empty pattern" $
      R.match B.empty (B.pack "text") `shouldReturn` True

    it "returns false with empty input and non-empty pattern" $
      R.match (B.pack "text") B.empty `shouldReturn` False

    it "handles special regex characters" $
      R.match (B.pack "a\\.b") (B.pack "a.b") `shouldReturn` True

    it "handles character class" $
      R.match (B.pack "[0-9]+") (B.pack "abc123def") `shouldReturn` True

    it "handles alternation" $
      R.match (B.pack "cat|dog") (B.pack "I have a dog") `shouldReturn` True

    it "handles unicode input" $
      R.match (B.pack "日本語") (B.pack "これは日本語です") `shouldReturn` True

    it "handles case sensitive match" $
      R.match (B.pack "Hello") (B.pack "hello") `shouldReturn` False

  describe "extractGroups" $ do
    it "extracts groups from pattern with capturing groups" $ do
      regex <- R.compile (B.pack "(\\w+)@(\\w+)")
      groups <- R.extractGroups regex (B.pack "user@domain")
      groups `shouldBe` [B.pack "user@domain", B.pack "user", B.pack "domain"]

    it "returns empty list when no match" $ do
      regex <- R.compile (B.pack "(foo)")
      groups <- R.extractGroups regex (B.pack "bar")
      groups `shouldBe` []

    it "extracts nested groups" $ do
      regex <- R.compile (B.pack "((a)(b))")
      groups <- R.extractGroups regex (B.pack "ab")
      groups `shouldBe` [B.pack "ab", B.pack "ab", B.pack "a", B.pack "b"]

    it "handles optional group that did not match" $ do
      regex <- R.compile (B.pack "(a)(b)?")
      groups <- R.extractGroups regex (B.pack "a")
      length groups `shouldBe` 3

    it "extracts multiple groups" $ do
      regex <- R.compile (B.pack "(x)(y)(z)")
      groups <- R.extractGroups regex (B.pack "prefix xyz suffix")
      groups `shouldBe` [B.pack "xyz", B.pack "x", B.pack "y", B.pack "z"]

    it "handles pattern without groups" $ do
      regex <- R.compile (B.pack "test")
      groups <- R.extractGroups regex (B.pack "this is a test")
      groups `shouldBe` [B.pack "test"]

  describe "substituteGroups" $ do
    it "substitutes group zero" $
      R.substituteGroups (B.pack "[$0]") [B.pack "match"] `shouldBe` B.pack "[match]"

    it "substitutes multiple groups" $
      R.substituteGroups (B.pack "$1-$2") [B.pack "full", B.pack "a", B.pack "b"]
        `shouldBe` B.pack "a-b"

    it "keeps dollar sign when no digits follow" $
      R.substituteGroups (B.pack "$ test") [B.pack "x"] `shouldBe` B.pack "$ test"

    it "keeps original reference for out of bounds index" $
      R.substituteGroups (B.pack "$9") [B.pack "only"] `shouldBe` B.pack "$9"

    it "handles replacement without group references" $
      R.substituteGroups (B.pack "plain") [B.pack "x"] `shouldBe` B.pack "plain"

    it "handles empty replacement" $
      R.substituteGroups B.empty [B.pack "x"] `shouldBe` B.empty

    it "handles empty groups list with reference" $
      R.substituteGroups (B.pack "$0") [] `shouldBe` B.pack "$0"

    it "handles multi-digit group reference" $
      R.substituteGroups (B.pack "$12") (replicate 13 (B.pack "x"))
        `shouldBe` B.pack "x"

    it "handles consecutive group references" $
      R.substituteGroups (B.pack "$0$1$2") [B.pack "a", B.pack "b", B.pack "c"]
        `shouldBe` B.pack "abc"

    it "handles unicode in replacement" $
      R.substituteGroups (B.pack "結果: $1") [B.pack "all", B.pack "データ"]
        `shouldBe` B.pack "結果: データ"

    it "handles dollar at end of string" $
      R.substituteGroups (B.pack "test$") [B.pack "x"] `shouldBe` B.pack "test$"

    it "handles double dollar" $
      R.substituteGroups (B.pack "$$1") [B.pack "x", B.pack "y"]
        `shouldBe` B.pack "$y"

  describe "replaceFirst" $ do
    it "replaces first occurrence" $ do
      regex <- R.compile (B.pack "cat")
      result <- R.replaceFirst regex (B.pack "dog") (B.pack "cat and cat")
      result `shouldBe` B.pack "dog and cat"

    it "returns input when no match" $ do
      regex <- R.compile (B.pack "xyz")
      result <- R.replaceFirst regex (B.pack "abc") (B.pack "hello world")
      result `shouldBe` B.pack "hello world"

    it "replaces with empty string" $ do
      regex <- R.compile (B.pack "remove")
      result <- R.replaceFirst regex B.empty (B.pack "please remove this")
      result `shouldBe` B.pack "please  this"

    it "replaces at start of string" $ do
      regex <- R.compile (B.pack "^start")
      result <- R.replaceFirst regex (B.pack "begin") (B.pack "start here")
      result `shouldBe` B.pack "begin here"

    it "replaces at end of string" $ do
      regex <- R.compile (B.pack "end$")
      result <- R.replaceFirst regex (B.pack "finish") (B.pack "the end")
      result `shouldBe` B.pack "the finish"

    it "uses captured groups in replacement" $ do
      regex <- R.compile (B.pack "(\\w+)@(\\w+)")
      result <- R.replaceFirst regex (B.pack "[$1 AT $2]") (B.pack "email: test@example here")
      result `shouldBe` B.pack "email: [test AT example] here"

    it "handles unicode pattern and replacement" $ do
      regex <- R.compile (B.pack "古い")
      result <- R.replaceFirst regex (B.pack "新しい") (B.pack "これは古いです")
      result `shouldBe` B.pack "これは新しいです"

    it "handles empty input" $ do
      regex <- R.compile (B.pack "x")
      result <- R.replaceFirst regex (B.pack "y") B.empty
      result `shouldBe` B.empty

    it "replaces entire string when pattern matches all" $ do
      regex <- R.compile (B.pack "^.*$")
      result <- R.replaceFirst regex (B.pack "replaced") (B.pack "original")
      result `shouldBe` B.pack "replaced"

  describe "replaceAll" $ do
    it "replaces all occurrences" $ do
      regex <- R.compile (B.pack "a")
      result <- R.replaceAll regex (B.pack "X") (B.pack "banana")
      result `shouldBe` B.pack "bXnXnX"

    it "returns input when no match" $ do
      regex <- R.compile (B.pack "xyz")
      result <- R.replaceAll regex (B.pack "abc") (B.pack "hello world")
      result `shouldBe` B.pack "hello world"

    it "replaces consecutive matches" $ do
      regex <- R.compile (B.pack "o")
      result <- R.replaceAll regex (B.pack "0") (B.pack "oooo")
      result `shouldBe` B.pack "0000"

    it "replaces with captured groups" $ do
      regex <- R.compile (B.pack "(\\d+)")
      result <- R.replaceAll regex (B.pack "[$1]") (B.pack "a1b2c3")
      result `shouldBe` B.pack "a[1]b[2]c[3]"

    it "handles empty replacement" $ do
      regex <- R.compile (B.pack "x")
      result <- R.replaceAll regex B.empty (B.pack "axbxcx")
      result `shouldBe` B.pack "abc"

    it "handles empty input" $ do
      regex <- R.compile (B.pack "x")
      result <- R.replaceAll regex (B.pack "y") B.empty
      result `shouldBe` B.empty

    it "handles unicode input and pattern" $ do
      regex <- R.compile (B.pack "кіт")
      result <- R.replaceAll regex (B.pack "пес") (B.pack "кіт і кіт")
      result `shouldBe` B.pack "пес і пес"

    it "replaces overlapping potential matches correctly" $ do
      regex <- R.compile (B.pack "aa")
      result <- R.replaceAll regex (B.pack "X") (B.pack "aaaa")
      result `shouldBe` B.pack "XX"

    it "handles single character replacement" $ do
      regex <- R.compile (B.pack ".")
      result <- R.replaceAll regex (B.pack "*") (B.pack "abc")
      result `shouldBe` B.pack "***"

    it "handles word boundary" $ do
      regex <- R.compile (B.pack "\\bword\\b")
      result <- R.replaceAll regex (B.pack "WORD") (B.pack "word in a word")
      result `shouldBe` B.pack "WORD in a WORD"