text-convert (empty) → 0.1.0.0
raw patch · 6 files changed
+542/−0 lines, 6 filesdep +QuickCheckdep +basedep +bytestring
Dependencies added: QuickCheck, base, bytestring, hspec, text, text-convert
Files
- LICENSE +26/−0
- README.md +49/−0
- src/tests-main/tests-main.hs +2/−0
- src/tests/TestsMain.hs +77/−0
- src/text-convert/Text/Convert.hs +303/−0
- text-convert.cabal +85/−0
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2025 Daniel Brice++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,49 @@+# text-convert++Convert between various types representing textual data. This library differs+from the similar 'string-conversions' library in that this library exports class+methods that are monomorphic in their return type. This enhances readability and+aids type inference.++++The module `Text.Convert` exports seven one-method type classes. The methods are+designed to be input-type polymorphic and output-type monomorphic, to aid with+readability and type inference. The classes are:+- `ToString`, with method `asString`;+- `ToByteString`, with method `asByteString`;+- `ToLazyByteString`, with method `asLazyByteString`;+- `ToByteStringBuilder`, with method `asByteStringBuilder`;+- `ToText`, with method `asText`;+- `ToLazyText`, with method `asLazyText`; and+- `ToTextBuilder`, with method `asTextBuilder`.++Design goals in order of importance:+1. correctness (including totality);+2. efficiency; and+3. ease of use.+The author hopes that the design achieves these goals to a high degree.++To comment on correctness, we are using lenient UTF-8 decoding when converting+between `ByteString`s and `Text`s, which means the conversions are correct in+the sense that they are total, but they are incorrect in the sense that they may+replace some unicode characters with the usual replacement character. Since this+library serves a very general purpose, users will not necessarily place any+restrictions on the inputs of these conversion functions, so the author thought+that totality was important. Notably, this choice agrees with the behavior of+many text editors and readers that use similar decoding strategies.++To comment on efficiency, we avoid unnecessary intermediate conversions where+possible. For example, instead of converting `Text` to `ByteString` through+`String` (a design admirable in that it is simple, correct, and a quantity of+code that is linear in number of supported datatypes), we choose to use a+bespoke conversion function for every pair of supported datatypes wherever+possible (resulting in a quadratic quantity of code). We also avoid unnecessary+allocation. For example, instead of converting `LazyByteString` to `Text`+through `LazyText` (an approach that would entail creating a new rope of `Text`+that would immediately be converted into a single array), we convert+`LazyByteString` to `Text` through `ByteString` (which is a single array). We+don't have any benchmarks to support this design choice, admittedly, as we+consider the cost of creating a benchmark suite overrides the benefits of having+one at this time. Should interest in this library grow, we may revisit this+decision.
+ src/tests-main/tests-main.hs view
@@ -0,0 +1,2 @@+module Main (main) where+import TestsMain (main)
+ src/tests/TestsMain.hs view
@@ -0,0 +1,77 @@+module TestsMain where++import Text.Convert++import Data.ByteString.Builder qualified as BB+import Data.ByteString.Char8 qualified as BC+import Data.ByteString.Lazy.Char8 qualified as BLC+import Data.Text qualified as T+import Data.Text.Encoding qualified as TE+import Data.Text.Encoding.Error qualified as TEE+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Builder qualified as TLB+import Data.Text.Lazy.Encoding qualified as TLE+import Data.Typeable (Typeable, typeRep)+import Test.Hspec (Spec, describe, hspec, shouldBe)+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck.Arbitrary (Arbitrary)++main :: IO ()+main = hspec $ do+ describe "asString" $ do+ identityProp asString id id "asString"+ weakInverseProp asString id id "BC.pack" BC.pack+ weakInverseProp asString id id "BLC.pack" BLC.pack+ weakInverseProp asString id id "T.pack" T.pack+ weakInverseProp asString id id "TL.pack" TL.pack+ weakInverseProp asString id id "TLB.fromString" TLB.fromString++ describe "asByteString" $ do+ identityProp asByteString BC.pack id "asByteString"+ weakInverseProp asByteString BC.pack id "BC.unpack" BC.unpack+ weakInverseProp asByteString BC.pack id "BLC.fromStrict" BLC.fromStrict+ weakInverseProp asByteString BC.pack id "TE.decodeUtf8With TEE.lenientDecode" (TE.decodeUtf8With TEE.lenientDecode)+ weakInverseProp asByteString BC.pack BB.toLazyByteString "BB.byteString" BB.byteString++ describe "asLazyByteString" $ do+ identityProp asLazyByteString BLC.pack id "asLazyByteString"+ weakInverseProp asLazyByteString BLC.pack id "id" BLC.unpack+ weakInverseProp asLazyByteString BLC.pack id "id" BLC.toStrict+ weakInverseProp asLazyByteString BLC.pack id "TLE.decodeUtf8With TEE.lenientDecode" (TLE.decodeUtf8With TEE.lenientDecode)+ weakInverseProp asLazyByteString BLC.pack BB.toLazyByteString "BB.lazyByteString" BB.lazyByteString++ describe "asText" $ do+ identityProp asText T.pack id "asText"+ weakInverseProp asText T.pack id "T.unpack" T.unpack+ weakInverseProp asText T.pack id "TE.encodeUtf8" TE.encodeUtf8+ weakInverseProp asText T.pack id "TL.fromStrict" TL.fromStrict+ weakInverseProp asText T.pack id "TLB.fromText" TLB.fromText++ describe "asLazyText" $ do+ identityProp asLazyText TL.pack id "asLazyText"+ weakInverseProp asLazyText TL.pack id "TL.unpack" TL.unpack+ weakInverseProp asLazyText TL.pack id "TLE.encodeUtf8" TLE.encodeUtf8+ weakInverseProp asLazyText TL.pack id "TL.toStrict" TL.toStrict+ weakInverseProp asLazyText TL.pack id "TLB.fromLazyText" TLB.fromLazyText++ describe "asTextBuilder" $ do+ identityProp asTextBuilder TLB.fromString id "asTextBuilder"+ weakInverseProp asTextBuilder TLB.fromString id "TLB.toLazyText" TLB.toLazyText++ describe "asByteStringBuilder" $ do+ identityProp asByteStringBuilder BB.stringUtf8 BB.toLazyByteString "asByteStringBuilder"+ weakInverseProp asByteStringBuilder BB.stringUtf8 id "BB.toLazyByteString" BB.toLazyByteString++identityProp :: forall a c t.+ (Arbitrary a, Typeable t, Show a, Show c, Eq c) =>+ (t -> t) -> (a -> t) -> (t -> c) -> String -> Spec+identityProp testFn gen cmp lbl =+ prop (lbl <> " should be the identity on " <> show (typeRep $ Nothing @t)) $+ \x -> (cmp . testFn . gen) x `shouldBe` (cmp . gen) x++weakInverseProp :: forall a c s t.+ (Arbitrary a, Typeable s, Show a, Show c, Eq c) =>+ (s -> t) -> (a -> t) -> (s -> c) -> String -> (t -> s) -> Spec+weakInverseProp testFn gen cmp lbl inv =+ prop (lbl <> " should be a weak inverse through " <> show (typeRep $ Nothing @s)) $+ \x -> (cmp . inv . testFn . inv . gen) x `shouldBe` (cmp . inv . gen) x
+ src/text-convert/Text/Convert.hs view
@@ -0,0 +1,303 @@+{- | Convert between various textual representation.++This module exports seven one-method type classes. The methods are+designed to be input-type polymorphic and output-type monomorphic, to aid with+readability and type inference. The classes are:+- `ToString`, with method `asString`;+- `ToByteString`, with method `asByteString`;+- `ToLazyByteString`, with method `asLazyByteString`;+- `ToByteStringBuilder`, with method `asByteStringBuilder`;+- `ToText`, with method `asText`;+- `ToLazyText`, with method `asLazyText`; and+- `ToTextBuilder`, with method `asTextBuilder`.++Design goals in order of importance:+1. correctness (including totality);+2. efficiency; and+3. ease of use.+The author hopes that the design achieves these goals to a high degree.++To comment on correctness, we are using lenient UTF-8 decoding when converting+between `ByteString`s and `Text`s, which means the conversions are correct in+the sense that they are total, but they are incorrect in the sense that they may+replace some unicode characters with the usual replacement character. Since this+library serves a very general purpose, users will not necessarily place any+restrictions on the inputs of these conversion functions, so the author thought+that totality was important. Notably, this choice agrees with the behavior of+many text editors and readers that use similar decoding strategies.++To comment on efficiency, we avoid unnecessary intermediate conversions where+possible. For example, instead of converting `Text` to `ByteString` through+`String` (a design admirable in that it is simple, correct, and a quantity of+code that is linear in number of supported datatypes), we choose to use a+bespoke conversion function for every pair of supported datatypes wherever+possible (resulting in a quadratic quantity of code). We also avoid unnecessary+allocation. For example, instead of converting `LazyByteString` to `Text`+through `LazyText` (an approach that would entail creating a new rope of `Text`+that would immediately be converted into a single array), we convert+`LazyByteString` to `Text` through `ByteString` (which is a single array). We+don't have any benchmarks to support this design choice, admittedly, as we+consider the cost of creating a benchmark suite overrides the benefits of having+one at this time. Should interest in this library grow, we may revisit this+decision.+-}+module Text.Convert (+ ToString(..),+ ToByteString(..),+ ToLazyByteString(..),+ ToByteStringBuilder(..),+ ToText(..),+ ToLazyText(..),+ ToTextBuilder(..),+ String,+ ByteString,+ LazyByteString,+ Text,+ LazyText,+) where++import Data.Text (Text)+import Data.ByteString (ByteString)+import Data.ByteString.Builder qualified as BB+import Data.ByteString.Char8 qualified as BC+import Data.ByteString.Lazy.Char8 qualified as BLC+import Data.Text qualified as T+import Data.Text.Encoding qualified as TE+import Data.Text.Encoding.Error qualified as TEE+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Builder qualified as TLB+import Data.Text.Lazy.Encoding qualified as TLE++type LazyByteString = BLC.ByteString+type LazyText = TL.Text++class ToString a where+ asString :: a -> String++class ToByteString a where+ asByteString :: a -> BC.ByteString++class ToLazyByteString a where+ asLazyByteString :: a -> BLC.ByteString++class ToByteStringBuilder a where+ asByteStringBuilder :: a -> BB.Builder++class ToText a where+ asText :: a -> T.Text++class ToLazyText a where+ asLazyText :: a -> TL.Text++class ToTextBuilder a where+ asTextBuilder :: a -> TLB.Builder++-- ToString instances++instance ToString String where+ asString = id+ {-# INLINE asString #-}++instance ToString BC.ByteString where+ asString = BC.unpack+ {-# INLINE asString #-}++instance ToString BLC.ByteString where+ asString = BLC.unpack+ {-# INLINE asString #-}++instance ToString BB.Builder where+ asString = BLC.unpack . BB.toLazyByteString+ {-# INLINE asString #-}++instance ToString T.Text where+ asString = T.unpack+ {-# INLINE asString #-}++instance ToString TL.Text where+ asString = TL.unpack+ {-# INLINE asString #-}++instance ToString TLB.Builder where+ asString = TL.unpack . TLB.toLazyText+ {-# INLINE asString #-}++-- ToByteString instances++instance ToByteString String where+ asByteString = BC.pack+ {-# INLINE asByteString #-}++instance ToByteString BC.ByteString where+ asByteString = id+ {-# INLINE asByteString #-}++instance ToByteString BLC.ByteString where+ asByteString = BLC.toStrict+ {-# INLINE asByteString #-}++instance ToByteString BB.Builder where+ asByteString = BLC.toStrict . BB.toLazyByteString+ {-# INLINE asByteString #-}++instance ToByteString T.Text where+ asByteString = TE.encodeUtf8+ {-# INLINE asByteString #-}++instance ToByteString TL.Text where+ asByteString = TE.encodeUtf8 . TL.toStrict+ {-# INLINE asByteString #-}++instance ToByteString TLB.Builder where+ asByteString = TE.encodeUtf8 . TL.toStrict . TLB.toLazyText+ {-# INLINE asByteString #-}++-- ToLazyByteString instances++instance ToLazyByteString String where+ asLazyByteString = BLC.pack+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString BC.ByteString where+ asLazyByteString = BLC.fromStrict+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString BLC.ByteString where+ asLazyByteString = id+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString BB.Builder where+ asLazyByteString = BB.toLazyByteString+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString T.Text where+ asLazyByteString = BLC.fromStrict . TE.encodeUtf8+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString TL.Text where+ asLazyByteString = TLE.encodeUtf8+ {-# INLINE asLazyByteString #-}++instance ToLazyByteString TLB.Builder where+ asLazyByteString = TLE.encodeUtf8 . TLB.toLazyText+ {-# INLINE asLazyByteString #-}++-- ToByteStringBuilder instances++instance ToByteStringBuilder String where+ asByteStringBuilder = BB.stringUtf8+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder BC.ByteString where+ asByteStringBuilder = BB.byteString+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder BLC.ByteString where+ asByteStringBuilder = BB.lazyByteString+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder BB.Builder where+ asByteStringBuilder = id+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder T.Text where+ asByteStringBuilder = BB.byteString . TE.encodeUtf8+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder TL.Text where+ asByteStringBuilder = BB.lazyByteString . TLE.encodeUtf8+ {-# INLINE asByteStringBuilder #-}++instance ToByteStringBuilder TLB.Builder where+ asByteStringBuilder = BB.lazyByteString . TLE.encodeUtf8 . TLB.toLazyText+ {-# INLINE asByteStringBuilder #-}++-- ToText instances++instance ToText String where+ asText = T.pack+ {-# INLINE asText #-}++instance ToText BC.ByteString where+ asText = TE.decodeUtf8With TEE.lenientDecode+ {-# INLINE asText #-}++instance ToText BLC.ByteString where+ asText = undefined+ {-# INLINE asText #-}++instance ToText BB.Builder where+ asText = undefined+ {-# INLINE asText #-}++instance ToText T.Text where+ asText = id+ {-# INLINE asText #-}++instance ToText TL.Text where+ asText = TL.toStrict+ {-# INLINE asText #-}++instance ToText TLB.Builder where+ asText = TL.toStrict . TLB.toLazyText+ {-# INLINE asText #-}++-- ToLazyText instances++instance ToLazyText String where+ asLazyText = TL.pack+ {-# INLINE asLazyText #-}++instance ToLazyText BC.ByteString where+ asLazyText = TL.fromStrict . TE.decodeUtf8With TEE.lenientDecode+ {-# INLINE asLazyText #-}++instance ToLazyText BLC.ByteString where+ asLazyText = TLE.decodeUtf8With TEE.lenientDecode+ {-# INLINE asLazyText #-}++instance ToLazyText BB.Builder where+ asLazyText = TLE.decodeUtf8With TEE.lenientDecode . BB.toLazyByteString+ {-# INLINE asLazyText #-}++instance ToLazyText T.Text where+ asLazyText = TL.fromStrict+ {-# INLINE asLazyText #-}++instance ToLazyText TL.Text where+ asLazyText = id+ {-# INLINE asLazyText #-}++instance ToLazyText TLB.Builder where+ asLazyText = TLB.toLazyText+ {-# INLINE asLazyText #-}++-- ToTextBuilder instances++instance ToTextBuilder String where+ asTextBuilder = TLB.fromString+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder BC.ByteString where+ asTextBuilder = TLB.fromText . TE.decodeUtf8With TEE.lenientDecode+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder BLC.ByteString where+ asTextBuilder = TLB.fromLazyText . TLE.decodeUtf8With TEE.lenientDecode+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder BB.Builder where+ asTextBuilder = TLB.fromLazyText . TLE.decodeUtf8With TEE.lenientDecode . BB.toLazyByteString+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder T.Text where+ asTextBuilder = TLB.fromText+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder TL.Text where+ asTextBuilder = TLB.fromLazyText+ {-# INLINE asTextBuilder #-}++instance ToTextBuilder TLB.Builder where+ asTextBuilder = id+ {-# INLINE asTextBuilder #-}
+ text-convert.cabal view
@@ -0,0 +1,85 @@+cabal-version: 2.2+name: text-convert+version: 0.1.0.0+category: Data+synopsis: Convert between various textual representation.+description:+ Convert between various types representing textual data. This library differs+ from the similar 'string-conversions' library in that this library exports+ class methods that are monomorphic in their return type. This enhances+ readability and aids type inference.++homepage: https://github.com/friedbrice/text-convert#readme+bug-reports: https://github.com/friedbrice/text-convert/issues+author: Daniel Brice+maintainer: danielbrice@gmail.com+copyright: 2025 Daniel Brice+license: BSD-3-Clause+license-file: LICENSE+extra-source-files: README.md+build-type: Simple+tested-with:+ GHC ==9.2.8+ || ==9.4.8+ || ==9.6.7+ || ==9.8.4+ || ==9.10.2+ || ==9.12.2++source-repository head+ type: git+ location: https://github.com/friedbrice/text-convert++common dialect+ default-language: Haskell2010+ ghc-options: -Wall+ default-extensions:+ FlexibleInstances+ ImportQualifiedPost+ ScopedTypeVariables+ TypeApplications++ build-depends:+ , base >=4.16 && <5+ , bytestring >=0.11 && <0.13+ , text >=1.2 && <2.2++common deps-tests+ build-depends:+ , hspec+ , QuickCheck+ , text-convert++library+ import: dialect+ hs-source-dirs: src/text-convert++ -- cabal-fmt: expand src/text-convert+ exposed-modules: Text.Convert++test-suite tests+ import: dialect+ import: deps-tests+ type: exitcode-stdio-1.0+ build-depends: text-convert+ hs-source-dirs: src/tests src/tests-main+ main-is: tests-main.hs++ -- cabal-fmt: expand src/tests+ other-modules: TestsMain++test-suite dev+ import: dialect+ import: deps-tests+ type: exitcode-stdio-1.0+ hs-source-dirs: src/text-convert src/tests src/tests-main+ main-is: tests-main.hs+ default-extensions:+ InstanceSigs+ PartialTypeSignatures++ -- cabal-fmt: expand src/text-convert+ -- cabal-fmt: expand src/tests+ other-modules:+ TestsMain+ Text.Convert