packages feed

idna2008-1.0.0.0: internal/Text/IDNA2008/Internal/Ranges.hs

-- |
-- Module      : Text.IDNA2008.Internal.Ranges
-- Description : Shared binary-search helper for sorted codepoint
--               range tables packed as @[start, end, start, end, ...]@
--               'Word32' entries in a 'ByteArray'.
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
--
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
--
-- One canonical implementation of @inRanges@ -- a binary search
-- over a sorted, coalesced array of @[start, end]@ pairs packed
-- into a 'ByteArray' as alternating 'Word32' entries.  Consumed
-- by the property-table wrappers ("Text.IDNA2008.Internal.Emoji",
-- "Text.IDNA2008.Internal.Joining",
-- "Text.IDNA2008.Internal.NFC", "Text.IDNA2008.Internal.Script")
-- when their range data is shaped that way.
--
-- The function is intentionally marked @INLINE@; callers
-- partially-apply it to a specific @arr@ and @cnt@ at the top
-- level, and GHC specialises the worker per call site.
{-# LANGUAGE BangPatterns #-}

module Text.IDNA2008.Internal.Ranges
    ( inRanges
    ) where

import Data.Array.Byte (ByteArray)
import Data.Primitive.ByteArray (indexByteArray)
import Data.Word (Word32)

-- | Test whether @cp@ falls within any of the @cnt@
-- @[start_i, end_i]@ ranges packed into @arr@ as alternating
-- 'Word32' entries, sorted ascending by @start_i@.
--
-- Returns 'False' when @cnt == 0@, and short-circuits @'False'@
-- when @cp@ precedes the first range's start.
inRanges :: ByteArray -> Int -> Int -> Bool
inRanges !arr !cnt !cp
    | cnt == 0 || cp < startAt 0 = False
    | otherwise =
        let !idx = bsearch 0 (cnt - 1)
        in cp <= endAt idx
  where
    startAt :: Int -> Int
    startAt !i = fromIntegral (indexByteArray arr (2 * i) :: Word32)
    {-# INLINE startAt #-}

    endAt :: Int -> Int
    endAt !i = fromIntegral (indexByteArray arr (2 * i + 1) :: Word32)
    {-# INLINE endAt #-}

    bsearch :: Int -> Int -> Int
    bsearch !lo !hi
      | lo == hi = lo
      | otherwise =
          let !mid = (lo + hi + 1) `quot` 2
          in if startAt mid <= cp
               then bsearch mid hi
               else bsearch lo (mid - 1)
{-# INLINE inRanges #-}