packages feed

postgresql-types-0.1: src/library/PostgresqlTypes/Line.hs

module PostgresqlTypes.Line
  ( Line,

    -- * Accessors
    toA,
    toB,
    toC,

    -- * Constructors
    refineFromEquation,
    normalizeFromEquation,
  )
where

import qualified Data.Attoparsec.Text as Attoparsec
import GHC.Float (castDoubleToWord64, castWord64ToDouble)
import PostgresqlTypes.Algebra
import PostgresqlTypes.Prelude
import PostgresqlTypes.Via
import qualified PtrPeeker
import qualified PtrPoker.Write as Write
import qualified Test.QuickCheck as QuickCheck
import qualified TextBuilder

-- | PostgreSQL @line@ type. Infinite line in 2D plane.
--
-- The line is represented by the linear equation @Ax + By + C = 0@.
-- Stored as three @64@-bit floating point numbers (@A@, @B@, @C@).
--
-- [PostgreSQL docs](https://www.postgresql.org/docs/18/datatype-geometric.html#DATATYPE-LINE).
data Line
  = Line
      -- | A coefficient
      Double
      -- | B coefficient
      Double
      -- | C coefficient
      Double
  deriving stock (Eq, Ord)
  deriving (Show, Read, IsString) via (ViaIsScalar Line)

instance Arbitrary Line where
  arbitrary = do
    -- Ensure at least one of A or B is non-zero
    (a, b) <-
      QuickCheck.suchThat
        ((,) <$> arbitrary <*> arbitrary)
        (\(a, b) -> not (a == 0 && b == 0))
    c <- arbitrary
    pure (Line a b c)
  shrink (Line a b c) =
    [ Line a' b' c'
    | (a', b', c') <- shrink (a, b, c),
      not (a' == 0 && b' == 0) -- Ensure shrunk values are also valid
    ]

instance Hashable Line where
  hashWithSalt salt (Line a b c) =
    salt
      `hashWithSalt` castDoubleToWord64 a
      `hashWithSalt` castDoubleToWord64 b
      `hashWithSalt` castDoubleToWord64 c

instance IsScalar Line where
  schemaName = Tagged Nothing
  typeName = Tagged "line"
  baseOid = Tagged (Just 628)
  arrayOid = Tagged (Just 629)
  typeParams = Tagged []
  binaryEncoder (Line a b c) =
    mconcat
      [ Write.bWord64 (castDoubleToWord64 a),
        Write.bWord64 (castDoubleToWord64 b),
        Write.bWord64 (castDoubleToWord64 c)
      ]
  binaryDecoder = do
    a <- PtrPeeker.fixed (castWord64ToDouble <$> PtrPeeker.beUnsignedInt8)
    b <- PtrPeeker.fixed (castWord64ToDouble <$> PtrPeeker.beUnsignedInt8)
    c <- PtrPeeker.fixed (castWord64ToDouble <$> PtrPeeker.beUnsignedInt8)
    pure (Right (Line a b c))
  textualEncoder (Line a b c) =
    "{"
      <> TextBuilder.string (printf "%g" a)
      <> ","
      <> TextBuilder.string (printf "%g" b)
      <> ","
      <> TextBuilder.string (printf "%g" c)
      <> "}"
  textualDecoder = do
    _ <- Attoparsec.char '{'
    a <- Attoparsec.double
    _ <- Attoparsec.char ','
    b <- Attoparsec.double
    _ <- Attoparsec.char ','
    c <- Attoparsec.double
    _ <- Attoparsec.char '}'
    pure (Line a b c)

-- * Accessors

-- | Extract the A coefficient from Ax + By + C = 0.
toA :: Line -> Double
toA (Line a _ _) = a

-- | Extract the B coefficient from Ax + By + C = 0.
toB :: Line -> Double
toB (Line _ b _) = b

-- | Extract the C coefficient from Ax + By + C = 0.
toC :: Line -> Double
toC (Line _ _ c) = c

-- * Constructors

-- | Construct a PostgreSQL 'Line' from equation coefficients A, B, C with validation.
-- Returns 'Nothing' if both A and B are zero (invalid line).
refineFromEquation :: Double -> Double -> Double -> Maybe Line
refineFromEquation a b c = do
  when (a == 0 && b == 0) empty
  pure (Line a b c)

-- | Construct a PostgreSQL 'Line' from equation coefficients A, B, C.
-- Defaults to vertical line when A and B equal 0.
normalizeFromEquation :: Double -> Double -> Double -> Line
normalizeFromEquation a b c =
  if a == 0 && b == 0
    then Line 1 0 c
    else Line a b c