diff --git a/ascii-char.cabal b/ascii-char.cabal
--- a/ascii-char.cabal
+++ b/ascii-char.cabal
@@ -1,11 +1,13 @@
-cabal-version: 2.0
+cabal-version: 3.0
 
 name: ascii-char
-version: 1.0.0.14
+version: 1.0.0.15
 synopsis: A Char type representing an ASCII character
 category: Data, Text
 
-description: This package defines a @Char@ type that has 128 constructors, one for each ASCII character.
+description:
+    This package defines a @Char@ type that has
+    128 constructors, one for each ASCII character.
 
 license: Apache-2.0
 license-file: license.txt
@@ -13,40 +15,52 @@
 author: Chris Martin
 maintainer: Chris Martin, Julie Moronuki
 
-homepage:    https://github.com/typeclasses/ascii
+homepage: https://github.com/typeclasses/ascii
 bug-Reports: https://github.com/typeclasses/ascii/issues
 
 build-type: Simple
 
-extra-source-files: changelog.txt
-
 source-repository head
-    type:     git
+    type: git
     location: git://github.com/typeclasses/ascii.git
 
-library
+common base
     default-language: Haskell2010
-    default-extensions: NoImplicitPrelude
-    default-extensions: DeriveAnyClass
-    default-extensions: DeriveDataTypeable
-    default-extensions: DeriveGeneric
-    default-extensions: StandaloneDeriving
-    default-extensions: DerivingStrategies
-    ghc-options: -Wall -fno-warn-unused-imports
+    ghc-options: -Wall
+
+    default-extensions:
+        NoImplicitPrelude
+
+    build-depends:
+        base >= 4.13 && < 4.17
+
+library
+    import: base
+    ghc-options: -fno-warn-unused-imports
     hs-source-dirs: library
 
-    build-depends: base     >= 4.11 && < 4.17
-    build-depends: hashable >= 1.2  && < 1.5
+    default-extensions:
+        DeriveAnyClass
+        DeriveDataTypeable
+        DeriveGeneric
+        DerivingStrategies
+        StandaloneDeriving
 
-    exposed-modules: ASCII.Char
+    build-depends:
+        hashable >= 1.3 && < 1.5
 
-test-suite test
+    exposed-modules:
+        ASCII.Char
+
+test-suite test-ascii-char
+    import: base
     type: exitcode-stdio-1.0
-    default-language: Haskell2010
-    default-extensions: NoImplicitPrelude
-    default-extensions: OverloadedStrings
-    default-extensions: QuasiQuotes
-    ghc-options: -Wall
     hs-source-dirs: test
-    main-is: test.hs
-    build-depends: ascii-char, base
+    main-is: Main.hs
+
+    default-extensions:
+        OverloadedStrings
+        QuasiQuotes
+
+    build-depends:
+        ascii-char
diff --git a/changelog.txt b/changelog.txt
deleted file mode 100644
--- a/changelog.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-1.0.0.0  - 2020-05-05 - Initial release
-1.0.0.2  - 2020-05-18 - Support GHC 8.10
-1.0.0.4  - 2021-01-25 - Add some comments
-1.0.0.6  - 2021-01-27 - Minor documentation fix
-1.0.0.8  - 2021-02-10 - Support GHC 9.0
-1.0.0.10 - 2021-09-26 - Add a test suite
-1.0.0.12 - 2021-11-13 - Support hashable 1.4
-1.0.0.14 - 2022-01-09 - Support GHC 9.2
diff --git a/library/ASCII/Char.hs b/library/ASCII/Char.hs
--- a/library/ASCII/Char.hs
+++ b/library/ASCII/Char.hs
@@ -13,17 +13,16 @@
   )
   where
 
-import Data.Bool     ( otherwise )
-import Data.Data     ( Data )
-import Data.Eq       ( Eq, (==), (/=) )
-import Data.Ord      ( Ord, (<), (>) )
-import Data.Hashable ( Hashable )
-import Data.Int      ( Int )
-import Data.Maybe    ( Maybe (..) )
-import Prelude       ( Enum, enumFromTo, toEnum, fromEnum,
-                       Bounded, minBound, maxBound )
-import Text.Show     ( Show )
-import GHC.Generics  ( Generic )
+import Data.Bool (otherwise)
+import Data.Data (Data)
+import Data.Eq (Eq, (/=), (==))
+import Data.Hashable (Hashable)
+import Data.Int (Int)
+import Data.Maybe (Maybe (..))
+import Data.Ord (Ord, (<), (>))
+import GHC.Generics (Generic)
+import Prelude (Bounded, Enum, enumFromTo, fromEnum, maxBound, minBound, toEnum)
+import Text.Show (Show)
 
 import qualified Data.Char as C
 
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,53 @@
+module Main (main) where
+
+import ASCII.Char
+
+import Control.Applicative (Applicative (..))
+import Control.Monad (Monad (..))
+import Data.Bool (Bool (..))
+import Data.Eq (Eq ((==)))
+import Data.Function (($), (.))
+import Data.Functor (Functor (..))
+import Data.List (intercalate, length, map, null)
+import Data.Maybe (Maybe (..))
+import Data.Semigroup ((<>))
+import Numeric.Natural (Natural)
+import Prelude (maxBound, minBound)
+import System.Exit (die)
+import System.IO (IO, putStrLn)
+import Text.Show (show)
+
+main :: IO ()
+main = dieIfFailures $ do
+    test 1 $ map toInt [Null, CapitalLetterA, SmallLetterA, Delete] == [0, 65, 97, 127]
+    test 2 $ map fromIntMaybe [-1, 0, 65, 127, 128] == [Nothing, Just Null, Just CapitalLetterA, Just Delete, Nothing]
+    test 3 $ map fromIntUnsafe [65, 66, 67] == [CapitalLetterA, CapitalLetterB, CapitalLetterC]
+    test 4 $ length allCharacters == 128
+    test 5 $ (minBound :: Char) == Null
+    test 6 $ (maxBound :: Char) == Delete
+
+dieIfFailures :: Failures a -> IO a
+dieIfFailures (Failures fs x) =
+    if null fs
+        then do putStrLn "💯"; return x
+        else die $ intercalate " " (map (("🔥" <> ) . show) fs)
+
+type TestNumber = Natural
+
+test :: TestNumber -> Bool -> Failures ()
+test n t = Failures (if t then [] else [n]) ()
+
+data Failures a = Failures [TestNumber] a
+
+instance Functor Failures
+  where
+    fmap f (Failures a x) = Failures a (f x)
+
+instance Applicative Failures
+  where
+    pure x = Failures [] x
+    Failures a f <*> Failures b x = Failures (a <> b) (f x)
+
+instance Monad Failures
+  where
+    Failures a x >>= f = let Failures b y = f x in Failures (a <> b) y
diff --git a/test/test.hs b/test/test.hs
deleted file mode 100644
--- a/test/test.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Main where
-
-import ASCII.Char
-
-import Control.Applicative (Applicative (..))
-import Control.Monad (Monad (..))
-import Data.Bool (Bool (..))
-import Data.Eq (Eq ((==)))
-import Data.Function ((.), ($))
-import Data.Functor (Functor (..))
-import Data.List (intercalate, map, null, length)
-import Data.Maybe (Maybe (..))
-import Data.Semigroup ((<>))
-import Numeric.Natural (Natural)
-import Prelude (minBound, maxBound)
-import System.Exit (die)
-import System.IO (IO, putStrLn)
-import Text.Show (show)
-
-main :: IO ()
-main = dieIfFailures $ do
-    test 1 $ map toInt [Null, CapitalLetterA, SmallLetterA, Delete] == [0, 65, 97, 127]
-    test 2 $ map fromIntMaybe [-1, 0, 65, 127, 128] == [Nothing, Just Null, Just CapitalLetterA, Just Delete, Nothing]
-    test 3 $ map fromIntUnsafe [65, 66, 67] == [CapitalLetterA, CapitalLetterB, CapitalLetterC]
-    test 4 $ length allCharacters == 128
-    test 5 $ (minBound :: Char) == Null
-    test 6 $ (maxBound :: Char) == Delete
-
-dieIfFailures :: Failures a -> IO a
-dieIfFailures (Failures fs x) =
-    if null fs
-        then do putStrLn "💯"; return x
-        else die $ intercalate " " (map (("🔥" <> ) . show) fs)
-
-type TestNumber = Natural
-
-test :: TestNumber -> Bool -> Failures ()
-test n t = Failures (if t then [] else [n]) ()
-
-data Failures a = Failures [TestNumber] a
-
-instance Functor Failures
-  where
-    fmap f (Failures a x) = Failures a (f x)
-
-instance Applicative Failures
-  where
-    pure x = Failures [] x
-    Failures a f <*> Failures b x = Failures (a <> b) (f x)
-
-instance Monad Failures
-  where
-    Failures a x >>= f = let Failures b y = f x in Failures (a <> b) y
