idna2008-1.0.0.0: internal/Text/IDNA2008/Internal/Width.hs
-- |
-- Module : Text.IDNA2008.Internal.Width
-- Description : Typed wrapper over the fullwidth\/halfwidth
-- decomposition table consumed by the @MAPWIDTH@
-- mapping (RFC 5895 section 2.2).
-- Copyright : (c) Viktor Dukhovni, 2026
-- License : BSD-3-Clause
--
-- Maintainer : ietf-dane@dukhovni.org
-- Stability : unstable
--
-- One lookup, 'widthMapCp', backed by binary search over the sorted
-- parallel arrays in "Text.IDNA2008.Internal.Width.Data".
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}
module Text.IDNA2008.Internal.Width
( widthMapCp
) where
import Data.Primitive.ByteArray (indexByteArray)
import Data.Word (Word32)
import Text.IDNA2008.Internal.Width.Data
( widthRangeCount
, widthSources
, widthTargets
)
-- | Apply the RFC 5895 section 2.2 fullwidth\/halfwidth mapping to
-- a single codepoint. Returns the input unchanged if it has no
-- @\<wide\>@ or @\<narrow\>@ decomposition. Codepoints absent
-- from the table (which is the vast majority) are unaffected.
widthMapCp :: Int -> Int
widthMapCp !cp
| widthRangeCount == 0 = cp
| cp < srcAt 0 = cp
| cp > srcAt (widthRangeCount - 1) = cp
| otherwise = case bsearch 0 (widthRangeCount - 1) of
Just i -> tgtAt i
Nothing -> cp
where
srcAt :: Int -> Int
srcAt !i = fromIntegral (indexByteArray widthSources i :: Word32)
{-# INLINE srcAt #-}
tgtAt :: Int -> Int
tgtAt !i = fromIntegral (indexByteArray widthTargets i :: Word32)
{-# INLINE tgtAt #-}
-- Standard binary search for an exact match.
bsearch :: Int -> Int -> Maybe Int
bsearch !lo !hi
| lo > hi = Nothing
| otherwise =
let !mid = (lo + hi) `quot` 2
!k = srcAt mid
in if | k == cp -> Just mid
| k < cp -> bsearch (mid + 1) hi
| otherwise -> bsearch lo (mid - 1)
{-# INLINE widthMapCp #-}