text-builder 0.6.10 → 1.0.0.5
raw patch · 15 files changed
Files
- CHANGELOG.md +34/−1
- README.md +184/−80
- bench/Main.hs +70/−67
- library/Text/Builder.hs +0/−7
- library/TextBuilder.hs +20/−255
- library/TextBuilder/Domains/ByteString.hs +41/−0
- library/TextBuilder/Domains/Combinators.hs +45/−0
- library/TextBuilder/Domains/Digits.hs +352/−0
- library/TextBuilder/Domains/Digits/Codepoints.hs +30/−0
- library/TextBuilder/Domains/Other.hs +12/−0
- library/TextBuilder/Prelude.hs +6/−0
- test/Main.hs +144/−77
- test/Util/ExtraInstances.hs +12/−0
- test/Util/TestTrees.hs +86/−0
- text-builder.cabal +66/−17
CHANGELOG.md view
@@ -1,4 +1,37 @@+# 1 +Major performance optimization achieved by getting rid of the "length" function. Since this was an unavoidable breaking change, other breaking changes were added as well.++Major redesign done to declutter the API from experimental features, dropping some and isolating others to "text-builder-dev" to reduce the need for further major releases of this library down the road. Order brought to naming conventions of the formatter-functions.++## Breaking changes and migration instructions++- `Text.Builder` module renamed to `TextBuilder` to fully conform to the [convention](https://www.reddit.com/r/haskell/comments/1qrilm/packages_and_namespaces_naming_convention/) of having the root namespace of the library map to its name.+- `Builder` type was renamed to `TextBuilder`.+- `run` renamed to `toText`.+- `length` removed to increase performance of the whole abstraction.+- `null` renamed to `isEmpty` to leave declarative naming only for constructor-functions.+- `putToStdOut`, `putToStdErr`, `putLnToStdOut`, `putLnToStdErr` removed to reduce the bloat of the API. Just use similar functions on `Text`.+- `padFromLeft` and `padFromRight` moved to "text-builder-dev-0.4".+- `asciiByteString` renamed to `unsafeUtf8ByteString`. ASCII is a subset of UTF-8 and the previous implementation was unsafe any way.+- `hexData` moved to "text-builder-dev-0.4" as `byteStringHexEncoding`.+- `unicodeCodePoint` renamed to `unicodeCodepoint`.+- `utf16CodeUnits1`, `utf16CodeUnits2`, `utf8CodeUnits1`, `utf8CodeUnits2`, `utf8CodeUnits3`, `utf8CodeUnits4` moved to "text-builder-core-0.1" and removed from the public API to reduce the bloat. Nobody seems to have been using them. If you need them, open a ticket in "text-builder-core".+- `unsignedDecimal` - removed because it was unsafe. Use `decimal`.+- `thousandSeparatedUnsignedDecimal` - removed because it was unsafe. Use `thousandSeparatedDecimal`.+- `dataSizeInBytesInDecimal` moved to "text-builder-dev-0.4" as `approximateDataSize`.+- `unsignedBinary` and `unsignedPaddedBinary` replaced with `binary`, which is now safe and handles negative values.+- `hexadecimal` and `unsignedHexadecimal` replaced with `hexadecimal`, which is now safe and handles negative values.+- `decimalDigit` and `hexadecimalDigit` removed. Seemed to not be useful. If you think otherwise open a ticket.+- `fixedDouble` moved to "text-builder-dev-0.4" as `doubleFixedPoint`. It does not have an efficient implementation yet is based on `printf`.+- `doublePercent` moved to "text-builder-dev-0.4" as `doubleFixedPointPercent`. Same reasons.+- `intervalInSeconds` moved to "text-builder-dev-0.4" as `diffTimeSeconds` and `picoseconds`.++## Non-breaking++- `unicodeCodepoint` is now safe. It replaces invalid codepoints with a default one.+- `char` is now safe for the same reasons.+ # 0.6.8 -- Migrated to the namespacing convention where the root namespace matches the package name 1-1 with no special cases. The support for previous naming convention is still provided though+- Migrated to the namespacing convention where the root namespace matches the package name 1-1 with no special cases. The support for previous naming convention is still provided though.
README.md view
@@ -1,94 +1,198 @@ # Summary -- Efficient monoidal builder of strict textual values-- Text formatting library, an alternative to `printf` or the "formatting" Haskell library+Fast strict text builder and simple type-safe formatting library for Haskell. # Performance -The benchmarks show that it's about 2 times faster than the lazy text builder supplied with the "text" package. In the years of existence of this package the collected user feedback proves that it's faster in fact.--```-benchmarking Left-biased mappend/1kB/TextBuilder-time 4.567 μs (4.552 μs .. 4.584 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 4.570 μs (4.551 μs .. 4.592 μs)-std dev 64.42 ns (49.19 ns .. 81.46 ns)-variance introduced by outliers: 12% (moderately inflated)--benchmarking Left-biased mappend/1kB/Data.Text.Lazy.Builder-time 8.855 μs (8.805 μs .. 8.901 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 8.867 μs (8.821 μs .. 8.908 μs)-std dev 137.9 ns (114.6 ns .. 160.4 ns)-variance introduced by outliers: 13% (moderately inflated)--benchmarking Left-biased mappend/1MB/TextBuilder-time 9.546 ms (9.415 ms .. 9.773 ms)- 0.998 R² (0.995 R² .. 1.000 R²)-mean 9.407 ms (9.351 ms .. 9.493 ms)-std dev 184.7 μs (92.85 μs .. 333.3 μs)--benchmarking Left-biased mappend/1MB/Data.Text.Lazy.Builder-time 17.04 ms (16.85 ms .. 17.21 ms)- 0.999 R² (0.999 R² .. 1.000 R²)-mean 17.09 ms (16.99 ms .. 17.19 ms)-std dev 228.4 μs (181.4 μs .. 285.3 μs)--benchmarking Right-biased mappend/1kB/TextBuilder-time 4.481 μs (4.467 μs .. 4.499 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 4.486 μs (4.470 μs .. 4.508 μs)-std dev 61.86 ns (44.45 ns .. 87.66 ns)-variance introduced by outliers: 11% (moderately inflated)--benchmarking Right-biased mappend/1kB/Data.Text.Lazy.Builder-time 8.751 μs (8.701 μs .. 8.798 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 8.751 μs (8.697 μs .. 8.805 μs)-std dev 159.2 ns (134.4 ns .. 205.0 ns)-variance introduced by outliers: 17% (moderately inflated)--benchmarking Right-biased mappend/1MB/TextBuilder-time 7.057 ms (7.035 ms .. 7.086 ms)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 7.071 ms (7.050 ms .. 7.093 ms)-std dev 57.85 μs (43.37 μs .. 83.06 μs)--benchmarking Right-biased mappend/1MB/Data.Text.Lazy.Builder-time 15.10 ms (14.96 ms .. 15.30 ms)- 0.999 R² (0.999 R² .. 1.000 R²)-mean 15.15 ms (15.04 ms .. 15.25 ms)-std dev 273.0 μs (208.6 μs .. 392.6 μs)+The benchmarks measure the monoidal composition of builders constructed from `Text` simulating left- and right-biased appends and `mconcat`. -benchmarking mconcat/1kB/TextBuilder-time 4.778 μs (4.756 μs .. 4.800 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 4.784 μs (4.770 μs .. 4.802 μs)-std dev 58.27 ns (48.36 ns .. 72.11 ns)+The results show the following: -benchmarking mconcat/1kB/Data.Text.Lazy.Builder-time 8.046 μs (8.009 μs .. 8.087 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 8.053 μs (8.021 μs .. 8.084 μs)-std dev 102.3 ns (81.50 ns .. 130.5 ns)+- It's 2-3 times faster than the `Data.Text.Lazy.Builder` supplied with the "text" package.+- It's 1.25 times faster than the `Data.Text.Encoding.StrictTextBuilder` which has recently arrived in "text".+- It's 1.2-2 times faster than "text-builder-linear".+- It's 1.1 times slower than `Data.Text.Text` in case of using `mconcat` over texts. In all other cases `Text` is slower and slows down exponentially, which is not surprising. -benchmarking mconcat/1MB/TextBuilder-time 8.902 ms (8.856 ms .. 8.931 ms)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 8.892 ms (8.868 ms .. 8.926 ms)-std dev 81.44 μs (62.67 μs .. 119.7 μs)+In the years of existence of this package multiple user-stories have been collected proving the performance boosts after switching to it. -benchmarking mconcat/1MB/Data.Text.Lazy.Builder-time 10.56 ms (10.39 ms .. 10.68 ms)- 0.999 R² (0.998 R² .. 0.999 R²)-mean 10.50 ms (10.42 ms .. 10.60 ms)-std dev 260.2 μs (191.2 μs .. 375.9 μs) ```+All+ Competition+ Left-biased mappend+ 100B+ TextBuilder.TextBuilder: OK+ 187 ns ± 14 ns, 960 B allocated, 0 B copied, 6.0 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 215 ns ± 17 ns, 1.2 KB allocated, 0 B copied, 6.0 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 510 ns ± 27 ns, 2.6 KB allocated, 1 B copied, 6.0 MB peak memory+ Data.Text.Text: OK+ 216 ns ± 15 ns, 1.6 KB allocated, 0 B copied, 6.0 MB peak memory+ Data.Text.Lazy.Text: OK+ 424 ns ± 30 ns, 4.2 KB allocated, 2 B copied, 6.0 MB peak memory+ Data.Text.Builder.Linear: OK+ 368 ns ± 26 ns, 1.3 KB allocated, 0 B copied, 6.0 MB peak memory+ 1kB+ TextBuilder.TextBuilder: OK+ 1.54 μs ± 144 ns, 8.3 KB allocated, 8 B copied, 6.0 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 1.85 μs ± 131 ns, 12 KB allocated, 10 B copied, 6.0 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 4.66 μs ± 458 ns, 24 KB allocated, 31 B copied, 6.0 MB peak memory+ Data.Text.Text: OK+ 3.80 μs ± 257 ns, 104 KB allocated, 85 B copied, 6.0 MB peak memory+ Data.Text.Lazy.Text: OK+ 24.5 μs ± 1.7 μs, 358 KB allocated, 451 B copied, 6.0 MB peak memory+ Data.Text.Builder.Linear: OK+ 3.04 μs ± 213 ns, 12 KB allocated, 6 B copied, 6.0 MB peak memory+ 10kB+ TextBuilder.TextBuilder: OK+ 15.5 μs ± 1.2 μs, 82 KB allocated, 175 B copied, 8.0 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 18.3 μs ± 536 ns, 121 KB allocated, 439 B copied, 8.0 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 47.7 μs ± 3.4 μs, 242 KB allocated, 1.9 KB copied, 8.0 MB peak memory+ Data.Text.Text: OK+ 355 μs ± 19 μs, 9.6 MB allocated, 19 KB copied, 13 MB peak memory+ Data.Text.Lazy.Text: OK+ 2.63 ms ± 251 μs, 34 MB allocated, 264 KB copied, 13 MB peak memory+ Data.Text.Builder.Linear: OK+ 30.6 μs ± 1.7 μs, 110 KB allocated, 202 B copied, 13 MB peak memory+ 100kB+ TextBuilder.TextBuilder: OK+ 201 μs ± 14 μs, 1.0 MB allocated, 14 KB copied, 13 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 195 μs ± 19 μs, 1.2 MB allocated, 39 KB copied, 13 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 520 μs ± 30 μs, 2.4 MB allocated, 174 KB copied, 13 MB peak memory+ Data.Text.Text: OK+ 28.1 ms ± 2.0 ms, 954 MB allocated, 476 KB copied, 25 MB peak memory+ Data.Text.Lazy.Text: OK+ 616 ms ± 34 ms, 4.7 GB allocated, 136 MB copied, 25 MB peak memory+ Data.Text.Builder.Linear: OK+ 361 μs ± 33 μs, 1.5 MB allocated, 15 KB copied, 25 MB peak memory+ Right-biased mappend+ 100B+ TextBuilder.TextBuilder: OK+ 188 ns ± 13 ns, 960 B allocated, 0 B copied, 25 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 231 ns ± 17 ns, 1.2 KB allocated, 0 B copied, 25 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 500 ns ± 34 ns, 2.6 KB allocated, 1 B copied, 25 MB peak memory+ Data.Text.Text: OK+ 214 ns ± 18 ns, 1.6 KB allocated, 0 B copied, 25 MB peak memory+ Data.Text.Lazy.Text: OK+ 283 ns ± 13 ns, 1.8 KB allocated, 0 B copied, 25 MB peak memory+ Data.Text.Builder.Linear: OK+ 363 ns ± 19 ns, 1.3 KB allocated, 0 B copied, 25 MB peak memory+ 1kB+ TextBuilder.TextBuilder: OK+ 1.59 μs ± 123 ns, 8.3 KB allocated, 7 B copied, 25 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 1.98 μs ± 120 ns, 12 KB allocated, 9 B copied, 25 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 4.66 μs ± 281 ns, 24 KB allocated, 30 B copied, 25 MB peak memory+ Data.Text.Text: OK+ 3.76 μs ± 72 ns, 104 KB allocated, 79 B copied, 25 MB peak memory+ Data.Text.Lazy.Text: OK+ 2.23 μs ± 215 ns, 17 KB allocated, 29 B copied, 25 MB peak memory+ Data.Text.Builder.Linear: OK+ 3.03 μs ± 249 ns, 12 KB allocated, 11 B copied, 25 MB peak memory+ 10kB+ TextBuilder.TextBuilder: OK+ 15.6 μs ± 1.0 μs, 82 KB allocated, 205 B copied, 25 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 32.7 μs ± 2.0 μs, 185 KB allocated, 556 B copied, 25 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 47.6 μs ± 4.1 μs, 242 KB allocated, 2.1 KB copied, 25 MB peak memory+ Data.Text.Text: OK+ 437 μs ± 14 μs, 9.6 MB allocated, 22 KB copied, 28 MB peak memory+ Data.Text.Lazy.Text: OK+ 24.9 μs ± 1.0 μs, 168 KB allocated, 1.9 KB copied, 28 MB peak memory+ Data.Text.Builder.Linear: OK+ 30.4 μs ± 1.7 μs, 110 KB allocated, 427 B copied, 28 MB peak memory+ 100kB+ TextBuilder.TextBuilder: OK+ 176 μs ± 11 μs, 820 KB allocated, 18 KB copied, 28 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 363 μs ± 34 μs, 1.8 MB allocated, 51 KB copied, 28 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 561 μs ± 41 μs, 2.4 MB allocated, 191 KB copied, 28 MB peak memory+ Data.Text.Text: OK+ 31.4 ms ± 1.5 ms, 954 MB allocated, 379 KB copied, 36 MB peak memory+ Data.Text.Lazy.Text: OK+ 315 μs ± 13 μs, 1.6 MB allocated, 182 KB copied, 36 MB peak memory+ Data.Text.Builder.Linear: OK+ 333 μs ± 26 μs, 1.3 MB allocated, 34 KB copied, 36 MB peak memory+ mconcat+ 100B+ TextBuilder.TextBuilder: OK+ 125 ns ± 6.6 ns, 496 B allocated, 0 B copied, 36 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 166 ns ± 13 ns, 1.0 KB allocated, 0 B copied, 36 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 324 ns ± 27 ns, 2.1 KB allocated, 0 B copied, 36 MB peak memory+ Data.Text.Text: OK+ 117 ns ± 7.2 ns, 280 B allocated, 0 B copied, 36 MB peak memory+ Data.Text.Lazy.Text: OK+ 212 ns ± 17 ns, 1.7 KB allocated, 0 B copied, 36 MB peak memory+ Data.Text.Builder.Linear: OK+ 175 ns ± 13 ns, 936 B allocated, 0 B copied, 36 MB peak memory+ 1kB+ TextBuilder.TextBuilder: OK+ 1.21 μs ± 104 ns, 3.6 KB allocated, 1 B copied, 36 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 1.69 μs ± 117 ns, 9.8 KB allocated, 10 B copied, 36 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 3.04 μs ± 301 ns, 20 KB allocated, 16 B copied, 36 MB peak memory+ Data.Text.Text: OK+ 1.07 μs ± 74 ns, 2.0 KB allocated, 1 B copied, 36 MB peak memory+ Data.Text.Lazy.Text: OK+ 1.76 μs ± 154 ns, 16 KB allocated, 20 B copied, 36 MB peak memory+ Data.Text.Builder.Linear: OK+ 1.42 μs ± 58 ns, 8.0 KB allocated, 3 B copied, 36 MB peak memory+ 10kB+ TextBuilder.TextBuilder: OK+ 15.0 μs ± 838 ns, 35 KB allocated, 5 B copied, 36 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 32.0 μs ± 2.3 μs, 162 KB allocated, 563 B copied, 36 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 34.8 μs ± 1.7 μs, 202 KB allocated, 927 B copied, 36 MB peak memory+ Data.Text.Text: OK+ 10.6 μs ± 899 ns, 20 KB allocated, 5 B copied, 36 MB peak memory+ Data.Text.Lazy.Text: OK+ 22.0 μs ± 1.7 μs, 160 KB allocated, 1.3 KB copied, 36 MB peak memory+ Data.Text.Builder.Linear: OK+ 16.3 μs ± 1.4 μs, 71 KB allocated, 13 B copied, 36 MB peak memory+ 100kB+ TextBuilder.TextBuilder: OK+ 176 μs ± 15 μs, 352 KB allocated, 55 B copied, 36 MB peak memory+ Data.Text.Encoding.StrictTextBuilder: OK+ 437 μs ± 21 μs, 1.8 MB allocated, 78 KB copied, 36 MB peak memory+ Data.Text.Lazy.Builder.Builder: OK+ 333 μs ± 26 μs, 2.0 MB allocated, 84 KB copied, 36 MB peak memory+ Data.Text.Text: OK+ 106 μs ± 10 μs, 195 KB allocated, 60 B copied, 36 MB peak memory+ Data.Text.Lazy.Text: OK+ 264 μs ± 15 μs, 1.6 MB allocated, 97 KB copied, 36 MB peak memory+ Data.Text.Builder.Linear: OK+ 170 μs ± 8.1 μs, 952 KB allocated, 166 B copied, 36 MB peak memory+ Features+ intercalate+ TextBuilder: OK+ 420 ns ± 28 ns, 3.8 KB allocated, 1 B copied, 36 MB peak memory+ Text: OK+ 143 ns ± 9.9 ns, 824 B allocated, 0 B copied, 36 MB peak memory+ binary: OK+ 62.8 ns ± 3.7 ns, 152 B allocated, 0 B copied, 36 MB peak memory+ octal: OK+ 25.1 ns ± 1.7 ns, 96 B allocated, 0 B copied, 36 MB peak memory+ decimal: OK+ 48.5 ns ± 3.8 ns, 560 B allocated, 0 B copied, 36 MB peak memory+ hexadecimal: OK+ 20.8 ns ± 2.1 ns, 104 B allocated, 0 B copied, 36 MB peak memory+``` # How it works It constructs text in two phases. In the first one it estimates the size of the byte array and in the second one it allocates it once and populates it in one go.--# What is text-builder-dev?--It is a lower-level library which this one wraps and provides a stable interface for. It serves as a testing ground for new features and design exploration.
bench/Main.hs view
@@ -1,78 +1,81 @@ module Main where -import Criterion.Main+import Data.Function+import qualified Data.Text as E+import qualified Data.Text.Builder.Linear+import qualified Data.Text.Encoding as D import qualified Data.Text.Lazy as C import qualified Data.Text.Lazy.Builder as B+import Test.Tasty.Bench import qualified TextBuilder as A import Prelude main :: IO () main = defaultMain- [ bgroup- "Left-biased mappend"- let {-# NOINLINE sampleBySize #-}- sampleBySize :: (Monoid a, IsString a) => Int -> (a -> Text) -> Text- sampleBySize size compile =- "фывапролдж"- & replicate size- & foldl' (<>) mempty- & compile- in [ bgroup- "1kB"- let sample = sampleBySize 100- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ],- bgroup- "1MB"- let sample = sampleBySize 100_000- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ]- ],- bgroup- "Right-biased mappend"- let {-# NOINLINE sampleBySize #-}- sampleBySize :: (Monoid a, IsString a) => Int -> (a -> Text) -> Text- sampleBySize size compile =- "фывапролдж"- & replicate size- & foldl' (flip (<>)) mempty- & compile- in [ bgroup- "1kB"- let sample = sampleBySize 100- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ],- bgroup- "1MB"- let sample = sampleBySize 100_000- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ]- ],- bgroup- "mconcat"- let {-# NOINLINE sampleBySize #-}- sampleBySize :: (Monoid a, IsString a) => Int -> (a -> Text) -> Text- sampleBySize size compile =- "фывапролдж"- & replicate size- & mconcat- & compile- in [ bgroup- "1kB"- let sample = sampleBySize 100- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ],- bgroup- "1MB"- let sample = sampleBySize 100_000- in [ bench "TextBuilder" (nf sample A.toText),- bench "Data.Text.Lazy.Builder" (nf sample (C.toStrict . B.toLazyText))- ]- ]+ [ bgroup "Competition" competition,+ bgroup "Features" features ]+ where+ competition =+ [ bgroup "Left-biased mappend" $ byConcat $ foldl' (<>) mempty,+ bgroup "Right-biased mappend" $ byConcat $ foldl' (flip (<>)) mempty,+ bgroup "mconcat" $ byConcat $ mconcat+ ]+ where+ byConcat (concat :: forall a. (Monoid a) => [a] -> a) =+ [ bgroup "100B" $ byTexts $ replicate 10 "фывапролдж",+ bgroup "1kB" $ byTexts $ replicate 100 "фывапролдж",+ bgroup "10kB" $ byTexts $ replicate 1_000 "фывапролдж",+ bgroup "100kB" $ byTexts $ replicate 10_000 "фывапролдж"+ ]+ where+ byTexts texts =+ [ bench+ "TextBuilder.TextBuilder"+ ( whnf+ (A.toText . concat)+ (fmap A.text texts)+ ),+ bench+ "Data.Text.Encoding.StrictTextBuilder"+ ( whnf+ (D.strictBuilderToText . concat)+ (fmap D.textToStrictBuilder texts)+ ),+ bench+ "Data.Text.Lazy.Builder.Builder"+ ( whnf+ (C.toStrict . B.toLazyText . concat)+ (fmap B.fromText texts)+ ),+ bench+ "Data.Text.Text"+ ( whnf+ concat+ texts+ ),+ bench+ "Data.Text.Lazy.Text"+ ( whnf+ (C.toStrict . concat)+ (fmap C.fromStrict texts)+ ),+ bench+ "Data.Text.Builder.Linear"+ ( whnf+ (Data.Text.Builder.Linear.runBuilder . concat)+ (fmap Data.Text.Builder.Linear.fromText texts)+ )+ ]++ features =+ [ bgroup "intercalate" $+ [ bench "TextBuilder" $ whnf (A.toText . A.intercalate "фывапролдж") ["a", "b", "c", "d", "e", "f"],+ bench "Text" $ whnf (E.intercalate "фывапролдж") ["a", "b", "c", "d", "e", "f"]+ ],+ bench "binary" $ whnf (A.toText . A.binary) (123456 :: Int),+ bench "octal" $ whnf (A.toText . A.octal) (123456 :: Int),+ bench "decimal" $ whnf (A.toText . A.decimal) (123456 :: Int),+ bench "hexadecimal" $ whnf (A.toText . A.hexadecimal) (123456 :: Int)+ ]
− library/Text/Builder.hs
@@ -1,7 +0,0 @@-module Text.Builder- {-# DEPRECATED "Use TextBuilder instead" #-}- ( module TextBuilder,- )-where--import TextBuilder
library/TextBuilder.hs view
@@ -1,286 +1,51 @@ module TextBuilder ( TextBuilder,- Builder, -- * Accessors toText,- run,- length,- null,-- -- ** Output IO- putToStdOut,- putToStdErr,- putLnToStdOut,- putLnToStdErr,+ toString,+ isEmpty, -- * Constructors - -- ** Builder manipulators+ -- ** Transformations+ force, intercalate,- padFromLeft,- padFromRight,+ intercalateMap, -- ** Textual text, lazyText, string,- asciiByteString,- hexData,+ unsafeUtf8ByteString, -- ** Character char,-- -- *** Low-level character- unicodeCodePoint,- utf16CodeUnits1,- utf16CodeUnits2,- utf8CodeUnits1,- utf8CodeUnits2,- utf8CodeUnits3,- utf8CodeUnits4,+ unicodeCodepoint, -- ** Integers -- *** Decimal decimal,- unsignedDecimal,+ fixedLengthDecimal, thousandSeparatedDecimal,- thousandSeparatedUnsignedDecimal,- dataSizeInBytesInDecimal, -- *** Binary- unsignedBinary,- unsignedPaddedBinary,+ binary,+ prefixedBinary, + -- *** Octal+ octal,+ prefixedOctal,+ -- *** Hexadecimal hexadecimal,- unsignedHexadecimal,-- -- ** Digits- decimalDigit,- hexadecimalDigit,-- -- ** Real- fixedDouble,- doublePercent,-- -- ** Time- intervalInSeconds,+ prefixedHexadecimal, ) where -import qualified Data.Text.Lazy as TextLazy-import TextBuilder.Prelude hiding (intercalate, length, null)-import qualified TextBuilderDev as Dev---- |--- Specification of how to efficiently construct strict 'Text'.--- Provides instances of 'Semigroup' and 'Monoid', which have complexity of /O(1)/.-newtype TextBuilder- = TextBuilder Dev.TextBuilder- deriving (Show, IsString, Semigroup, Monoid)--{-# DEPRECATED Builder "Use TextBuilder instead" #-}--type Builder = TextBuilder---- | Get the amount of characters-{-# INLINE length #-}-length :: TextBuilder -> Int-length = coerce Dev.length---- | Check whether the builder is empty-{-# INLINE null #-}-null :: TextBuilder -> Bool-null = coerce Dev.null---- | Execute a builder producing a strict text-toText :: TextBuilder -> Text-toText = coerce Dev.toText--{-# DEPRECATED run "Use toText instead" #-}---- | Alias to 'toText'-run :: TextBuilder -> Text-run = toText---- ** Output IO---- | Put builder, to stdout-putToStdOut :: TextBuilder -> IO ()-putToStdOut = coerce Dev.putToStdOut---- | Put builder, to stderr-putToStdErr :: TextBuilder -> IO ()-putToStdErr = coerce Dev.putToStdErr---- | Put builder, followed by a line, to stdout-putLnToStdOut :: TextBuilder -> IO ()-putLnToStdOut = coerce Dev.putLnToStdOut---- | Put builder, followed by a line, to stderr-putLnToStdErr :: TextBuilder -> IO ()-putLnToStdErr = coerce Dev.putLnToStdErr---- * Constructors---- | Unicode character-{-# INLINE char #-}-char :: Char -> TextBuilder-char = coerce Dev.char---- | Unicode code point-{-# INLINE unicodeCodePoint #-}-unicodeCodePoint :: Int -> TextBuilder-unicodeCodePoint = coerce Dev.unicodeCodePoint---- | Single code-unit UTF-16 character-{-# INLINEABLE utf16CodeUnits1 #-}-utf16CodeUnits1 :: Word16 -> TextBuilder-utf16CodeUnits1 = coerce Dev.utf16CodeUnits1---- | Double code-unit UTF-16 character-{-# INLINEABLE utf16CodeUnits2 #-}-utf16CodeUnits2 :: Word16 -> Word16 -> TextBuilder-utf16CodeUnits2 = coerce Dev.utf16CodeUnits2---- | Single code-unit UTF-8 character-{-# INLINE utf8CodeUnits1 #-}-utf8CodeUnits1 :: Word8 -> TextBuilder-utf8CodeUnits1 = coerce Dev.utf8CodeUnits1---- | Double code-unit UTF-8 character-{-# INLINE utf8CodeUnits2 #-}-utf8CodeUnits2 :: Word8 -> Word8 -> TextBuilder-utf8CodeUnits2 = coerce Dev.utf8CodeUnits2---- | Triple code-unit UTF-8 character-{-# INLINE utf8CodeUnits3 #-}-utf8CodeUnits3 :: Word8 -> Word8 -> Word8 -> TextBuilder-utf8CodeUnits3 = coerce Dev.utf8CodeUnits3---- | UTF-8 character out of 4 code units-{-# INLINE utf8CodeUnits4 #-}-utf8CodeUnits4 :: Word8 -> Word8 -> Word8 -> Word8 -> TextBuilder-utf8CodeUnits4 = coerce Dev.utf8CodeUnits4---- | ASCII byte string-{-# INLINE asciiByteString #-}-asciiByteString :: ByteString -> TextBuilder-asciiByteString = coerce Dev.asciiByteString---- | Strict text-{-# INLINE text #-}-text :: Text -> TextBuilder-text = coerce Dev.text---- | Lazy text-{-# INLINE lazyText #-}-lazyText :: TextLazy.Text -> TextBuilder-lazyText = coerce Dev.lazyText---- | String-{-# INLINE string #-}-string :: String -> TextBuilder-string = coerce Dev.string---- | Decimal representation of an integral value-{-# INLINEABLE decimal #-}-decimal :: (Integral a) => a -> TextBuilder-decimal = coerce . Dev.decimal---- | Decimal representation of an unsigned integral value-{-# INLINEABLE unsignedDecimal #-}-unsignedDecimal :: (Integral a) => a -> TextBuilder-unsignedDecimal = coerce . Dev.unsignedDecimal---- | Decimal representation of an integral value with thousands separated by the specified character-{-# INLINEABLE thousandSeparatedDecimal #-}-thousandSeparatedDecimal :: (Integral a) => Char -> a -> TextBuilder-thousandSeparatedDecimal = fmap coerce . Dev.thousandSeparatedDecimal---- | Decimal representation of an unsigned integral value with thousands separated by the specified character-{-# INLINEABLE thousandSeparatedUnsignedDecimal #-}-thousandSeparatedUnsignedDecimal :: (Integral a) => Char -> a -> TextBuilder-thousandSeparatedUnsignedDecimal = fmap coerce . Dev.thousandSeparatedUnsignedDecimal---- | Data size in decimal notation over amount of bytes.-{-# INLINEABLE dataSizeInBytesInDecimal #-}-dataSizeInBytesInDecimal :: (Integral a) => Char -> a -> TextBuilder-dataSizeInBytesInDecimal = fmap coerce . Dev.dataSizeInBytesInDecimal---- | Unsigned binary number-{-# INLINE unsignedBinary #-}-unsignedBinary :: (Integral a) => a -> TextBuilder-unsignedBinary = coerce . Dev.unsignedBinary---- | Unsigned binary number-{-# INLINE unsignedPaddedBinary #-}-unsignedPaddedBinary :: (Integral a, FiniteBits a) => a -> TextBuilder-unsignedPaddedBinary = coerce . Dev.unsignedPaddedBinary---- | Hexadecimal representation of an integral value-{-# INLINE hexadecimal #-}-hexadecimal :: (Integral a) => a -> TextBuilder-hexadecimal = coerce . Dev.hexadecimal---- | Unsigned hexadecimal representation of an integral value-{-# INLINE unsignedHexadecimal #-}-unsignedHexadecimal :: (Integral a) => a -> TextBuilder-unsignedHexadecimal = coerce . Dev.unsignedHexadecimal---- | Decimal digit-{-# INLINE decimalDigit #-}-decimalDigit :: (Integral a) => a -> TextBuilder-decimalDigit = coerce . Dev.decimalDigit---- | Hexadecimal digit-{-# INLINE hexadecimalDigit #-}-hexadecimalDigit :: (Integral a) => a -> TextBuilder-hexadecimalDigit = coerce . Dev.hexadecimalDigit---- | Intercalate builders-{-# INLINE intercalate #-}-intercalate :: (Foldable foldable) => TextBuilder -> foldable TextBuilder -> TextBuilder-intercalate a b = coerce (Dev.intercalate (coerce a) (foldr ((:) . coerce) [] b))---- | Pad a builder from the left side to the specified length with the specified character-{-# INLINEABLE padFromLeft #-}-padFromLeft :: Int -> Char -> TextBuilder -> TextBuilder-padFromLeft = coerce Dev.padFromLeft---- | Pad a builder from the right side to the specified length with the specified character-{-# INLINEABLE padFromRight #-}-padFromRight :: Int -> Char -> TextBuilder -> TextBuilder-padFromRight = coerce Dev.padFromRight---- |--- Time interval in seconds.--- Directly applicable to 'DiffTime' and 'NominalDiffTime'.-{-# INLINEABLE intervalInSeconds #-}-intervalInSeconds :: (RealFrac seconds) => seconds -> TextBuilder-intervalInSeconds = coerce . Dev.intervalInSeconds---- | Double with a fixed number of decimal places.-{-# INLINE fixedDouble #-}-fixedDouble ::- -- | Amount of decimals after point.- Int ->- Double ->- TextBuilder-fixedDouble = coerce Dev.fixedDouble---- | Double multiplied by 100 with a fixed number of decimal places applied and followed by a percent-sign.-{-# INLINE doublePercent #-}-doublePercent ::- -- | Amount of decimals after point.- Int ->- Double ->- TextBuilder-doublePercent = coerce Dev.doublePercent---- | Hexadecimal readable representation of binary data.-{-# INLINE hexData #-}-hexData :: ByteString -> TextBuilder-hexData = coerce Dev.hexData+import TextBuilder.Domains.ByteString+import TextBuilder.Domains.Combinators+import TextBuilder.Domains.Digits+import TextBuilder.Domains.Other+import TextBuilderCore
+ library/TextBuilder/Domains/ByteString.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE CPP #-}++module TextBuilder.Domains.ByteString where++import qualified Data.ByteString as ByteString+import qualified Data.Text.Array as TextArray+import TextBuilder.Prelude+import TextBuilderCore++#if !MIN_VERSION_text(2,0,0)+import qualified Data.Text.Encoding as TextEncoding+#endif++-- | UTF-8 bytestring. You can use it for converting ASCII values as well.+--+-- __Warning:__ It's your responsibility to ensure that the bytestring is properly encoded.+--+-- >>> unsafeUtf8ByteString "abc"+-- "abc"+--+-- >>> import Data.Text.Encoding (encodeUtf8)+-- >>> unsafeUtf8ByteString (encodeUtf8 "фывапролдж") == "фывапролдж"+-- True+{-# INLINEABLE unsafeUtf8ByteString #-}+unsafeUtf8ByteString :: ByteString -> TextBuilder+#if MIN_VERSION_text(2,0,0)+unsafeUtf8ByteString byteString =+ TextBuilder+ (ByteString.length byteString)+ ( \array ->+ -- TODO: Optimize to use memcpy or something similar.+ let step byte next index = do+ TextArray.unsafeWrite array index byte+ next (succ index)+ in ByteString.foldr step return byteString+ )+#else+-- Using a suboptimal solution here since the older version of \"text\" is becoming less important with time.+unsafeUtf8ByteString =+ text . TextEncoding.decodeUtf8+#endif
+ library/TextBuilder/Domains/Combinators.hs view
@@ -0,0 +1,45 @@+module TextBuilder.Domains.Combinators where++import TextBuilder.Prelude hiding (intercalate)+import TextBuilderCore++-- |+-- Run the builder and pack the produced text into a new builder.+--+-- Useful to have around builders that you reuse,+-- because a forced builder is much faster,+-- since it's virtually a single call to @memcopy@.+{-# INLINE force #-}+force :: TextBuilder -> TextBuilder+force = text . toText++-- | Intercalate builders.+--+-- >>> intercalate ", " ["a", "b", "c"]+-- "a, b, c"+--+-- >>> intercalate ", " ["a"]+-- "a"+--+-- >>> intercalate ", " []+-- ""+{-# INLINE intercalate #-}+intercalate :: (Foldable f) => TextBuilder -> f TextBuilder -> TextBuilder+intercalate separator elements =+ foldr+ (\element next prefix -> prefix <> element <> next separator)+ (const mempty)+ elements+ mempty++-- | Intercalate projecting values to builder.+{-# INLINE intercalateMap #-}+intercalateMap :: (Foldable f) => TextBuilder -> (a -> TextBuilder) -> f a -> TextBuilder+intercalateMap separator mapper = extract . foldl' step init+ where+ init = Nothing+ step acc element =+ Just $ case acc of+ Nothing -> mapper element+ Just acc -> acc <> separator <> mapper element+ extract = fromMaybe mempty
+ library/TextBuilder/Domains/Digits.hs view
@@ -0,0 +1,352 @@+module TextBuilder.Domains.Digits where++import qualified Data.Text.Array as TextArray+import qualified TextBuilder.Domains.Digits.Codepoints as Codepoints+import TextBuilder.Prelude+import TextBuilderCore++-- | Decimal digit.+{-# INLINE decimalDigit #-}+decimalDigit :: (Integral a) => a -> TextBuilder+decimalDigit (fromIntegral -> n) =+ unicodeCodepoint (n + 48)++-- | Hexadecimal digit.+{-# INLINE hexadecimalDigit #-}+hexadecimalDigit :: (Integral a) => a -> TextBuilder+hexadecimalDigit (fromIntegral -> n) =+ if n <= 9+ then unicodeCodepoint (n + 48)+ else unicodeCodepoint (n + 87)++{-# INLINE customFixedNumeralSystem #-}+customFixedNumeralSystem ::+ (FiniteBits a, Integral a) =>+ -- | Number of bits per digit.+ Int ->+ -- | Projection to codepoint with handling of overflow.+ (a -> a) ->+ -- | Value.+ a ->+ TextBuilder+customFixedNumeralSystem bitsPerDigit digitCodepoint val =+ let size = div (finiteBitSize val + bitsPerDigit - 1) bitsPerDigit+ in TextBuilder size \array arrayStartIndex ->+ let go val arrayIndex =+ if arrayIndex >= arrayStartIndex+ then do+ TextArray.unsafeWrite array arrayIndex (fromIntegral (digitCodepoint val))+ go (unsafeShiftR val bitsPerDigit) (pred arrayIndex)+ else return indexAfter+ indexAfter =+ arrayStartIndex + size+ in go val (pred indexAfter)++-- |+-- [Two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) binary representation of a value.+--+-- Bits of a statically sized value padded from the left according to the size.+-- If it's a negatable integer, the sign is reflected in the bits.+--+-- >>> binary @Int8 0+-- "00000000"+--+-- >>> binary @Int8 4+-- "00000100"+--+-- >>> binary @Int8 (-1)+-- "11111111"+--+-- >>> binary @Word8 255+-- "11111111"+--+-- >>> binary @Int16 4+-- "0000000000000100"+--+-- >>> binary @Int16 (-4)+-- "1111111111111100"+{-# INLINE binary #-}+binary :: (FiniteBits a) => a -> TextBuilder+binary val =+ let size = finiteBitSize val+ in TextBuilder size \array arrayStartIndex ->+ let go val arrayIndex =+ if arrayIndex >= arrayStartIndex+ then do+ TextArray.unsafeWrite array arrayIndex if testBit val 0 then 49 else 48+ go (unsafeShiftR val 1) (pred arrayIndex)+ else return indexAfter+ indexAfter =+ arrayStartIndex + size+ in go val (pred indexAfter)++-- |+-- Same as 'binary', but with the \"0b\" prefix.+--+-- >>> prefixedBinary @Int8 0+-- "0b00000000"+{-# INLINE prefixedBinary #-}+prefixedBinary :: (FiniteBits a) => a -> TextBuilder+prefixedBinary = mappend "0b" . binary++-- | Octal representation of an integer.+--+-- >>> octal @Int32 123456+-- "00000361100"+--+-- >>> octal @Int32 (-123456)+-- "77777416700"+{-# INLINE octal #-}+octal :: (FiniteBits a, Integral a) => a -> TextBuilder+octal = customFixedNumeralSystem 3 (Codepoints.octalDigit . fromIntegral)++-- |+-- Same as 'octal', but with the \"0o\" prefix.+--+-- >>> prefixedOctal @Int8 0+-- "0o000"+{-# INLINE prefixedOctal #-}+prefixedOctal :: (FiniteBits a, Integral a) => a -> TextBuilder+prefixedOctal = mappend "0o" . octal++-- | Integer in hexadecimal notation with a fixed number of digits determined by the size of the type.+--+-- >>> hexadecimal @Int8 0+-- "00"+--+-- >>> hexadecimal @Int8 4+-- "04"+--+-- >>> hexadecimal @Int8 (-128)+-- "80"+--+-- >>> hexadecimal @Int8 (-1)+-- "ff"+--+-- >>> hexadecimal @Word8 255+-- "ff"+--+-- >>> hexadecimal @Int32 123456+-- "0001e240"+--+-- >>> hexadecimal @Int32 (-123456)+-- "fffe1dc0"+{-# INLINE hexadecimal #-}+hexadecimal :: (FiniteBits a, Integral a) => a -> TextBuilder+hexadecimal = customFixedNumeralSystem 4 (Codepoints.hexDigit . fromIntegral)++-- |+-- Same as 'hexadecimal', but with the \"0x\" prefix.+--+-- >>> prefixedHexadecimal @Int8 0+-- "0x00"+{-# INLINE prefixedHexadecimal #-}+prefixedHexadecimal :: (FiniteBits a, Integral a) => a -> TextBuilder+prefixedHexadecimal = mappend "0x" . hexadecimal++-- * Signed Numbers++{-# INLINE signed #-}+signed :: (Integral a) => (forall a. (Integral a) => a -> TextBuilder) -> a -> TextBuilder+signed onUnsigned a =+ if a >= 0+ then onUnsigned a+ else+ unicodeCodepoint 45+ <> let negated = negate a+ in if negated /= a+ then onUnsigned negated+ else+ -- This is a special case for the minimum value of signed types.+ -- The negation of the minimum value is not representable in the same type.+ -- For example, for Int8, -128 is not representable as a positive number.+ onUnsigned (negate (fromIntegral a :: Integer))++-- | Signed decimal representation of an integer.+--+-- >>> decimal 123456+-- "123456"+--+-- >>> decimal (-123456)+-- "-123456"+--+-- >>> decimal 0+-- "0"+--+-- >>> decimal (-2 :: Int8)+-- "-2"+--+-- >>> decimal (-128 :: Int8)+-- "-128"+{-# INLINE decimal #-}+decimal :: (Integral a) => a -> TextBuilder+decimal = signed unsignedDecimal++-- * Unsigned Numbers++-- | Render a number in the given radix.+{-# INLINE digitsByRadix #-}+digitsByRadix :: (Integral a) => a -> (a -> a) -> a -> TextBuilder+digitsByRadix radix digitCodepoint =+ go 0 []+ where+ go !offset !digits x = case divMod x radix of+ (next, digit) ->+ if next <= 0+ then finish (succ offset) (digit : digits)+ else go (succ offset) (digit : digits) next++ finish size digits =+ TextBuilder size action+ where+ action :: TextArray.MArray s -> Int -> ST s Int+ action array =+ go digits+ where+ go digits offset = case digits of+ [] -> return offset+ (digit : digits) -> do+ TextArray.unsafeWrite array offset (fromIntegral (digitCodepoint digit))+ go digits (succ offset)++-- | Unsigned octal representation of a non-negative integral value.+--+-- __Warning:__ It is your responsibility to ensure that the value is non-negative.+--+--+-- >>> unsignedOctal 7+-- "7"+--+-- >>> unsignedOctal 9+-- "11"+--+-- >>> unsignedOctal 16+-- "20"+{-# INLINE unsignedOctal #-}+unsignedOctal :: (Integral a) => a -> TextBuilder+unsignedOctal =+ digitsByRadix 8 (+ 48)++-- | Unsigned decimal representation of a non-negative integral value.+--+-- __Warning:__ It is your responsibility to ensure that the value is non-negative.+--+-- >>> unsignedDecimal 123456+-- "123456"+--+-- >>> unsignedDecimal 0+-- "0"+{-# INLINE unsignedDecimal #-}+unsignedDecimal :: (Integral a) => a -> TextBuilder+unsignedDecimal =+ digitsByRadix 10 (+ 48)++-- | Unsigned hexadecimal representation of a non-negative integral value.+--+-- __Warning:__ It is your responsibility to ensure that the value is non-negative.+--+-- >>> unsignedHexadecimal 123456+-- "1e240"+--+-- >>> unsignedHexadecimal 0+-- "0"+{-# INLINE unsignedHexadecimal #-}+unsignedHexadecimal :: (Integral a) => a -> TextBuilder+unsignedHexadecimal =+ digitsByRadix 16 (\digit -> if digit <= 9 then digit + 48 else digit + 87)++-- * Other++-- | Fixed-length decimal without sign.+-- Padded with zeros or trimmed depending on whether it's shorter or longer+-- than specified.+--+-- >>> fixedLengthDecimal 5 123+-- "00123"+--+-- >>> fixedLengthDecimal 5 123456+-- "23456"+--+-- >>> fixedLengthDecimal 5 (-123456)+-- "23456"+--+-- >>> fixedLengthDecimal 7 (-123456)+-- "0123456"+--+-- >>> fixedLengthDecimal 0 123+-- ""+--+-- >>> fixedLengthDecimal (-2) 123+-- ""+{-# INLINEABLE fixedLengthDecimal #-}+fixedLengthDecimal :: (Integral a) => Int -> a -> TextBuilder+fixedLengthDecimal (max 0 -> size) (abs -> val) =+ TextBuilder size $ \array startOffset ->+ let offsetAfter = startOffset + size+ writeValue val offset =+ if offset >= startOffset+ then+ if val /= 0+ then case divMod val 10 of+ (val, digit) -> do+ TextArray.unsafeWrite array offset $ 48 + fromIntegral digit+ writeValue val (pred offset)+ else writePadding offset+ else return offsetAfter+ writePadding offset =+ if offset >= startOffset+ then do+ TextArray.unsafeWrite array offset 48+ writePadding (pred offset)+ else return offsetAfter+ in writeValue val (pred offsetAfter)++-- | Decimal representation of an integral value with thousands separated by the specified character.+--+-- >>> thousandSeparatedDecimal ',' 1234567890+-- "1,234,567,890"+--+-- >>> thousandSeparatedDecimal ' ' (-1234567890)+-- "-1 234 567 890"+{-# INLINEABLE thousandSeparatedDecimal #-}+thousandSeparatedDecimal :: (Integral a) => Char -> a -> TextBuilder+thousandSeparatedDecimal separatorChar =+ signed (unsignedThousandSeparatedDecimal separatorChar)++-- | Decimal representation of an unsigned integral value with thousands separated by the specified character.+--+-- __Warning:__ It is your responsibility to ensure that the value is non-negative.+--+-- >>> unsignedThousandSeparatedDecimal ',' 1234567890+-- "1,234,567,890"+--+-- >>> unsignedThousandSeparatedDecimal ' ' 1234567890+-- "1 234 567 890"+--+-- >>> unsignedThousandSeparatedDecimal ',' 0+-- "0"+{-# INLINEABLE unsignedThousandSeparatedDecimal #-}+unsignedThousandSeparatedDecimal :: (Integral a) => Char -> a -> TextBuilder+unsignedThousandSeparatedDecimal separatorChar =+ processRightmostDigit+ where+ processRightmostDigit value =+ case divMod value 10 of+ (value, digit) ->+ processAnotherDigit [decimalDigit digit] (1 :: Int) value+ processAnotherDigit builders index value =+ if value == 0+ then mconcat builders+ else case divMod value 10 of+ (value, digit) ->+ if mod index 3 == 0+ then+ processAnotherDigit+ (decimalDigit digit : char separatorChar : builders)+ (succ index)+ value+ else+ processAnotherDigit+ (decimalDigit digit : builders)+ (succ index)+ value
+ library/TextBuilder/Domains/Digits/Codepoints.hs view
@@ -0,0 +1,30 @@+module TextBuilder.Domains.Digits.Codepoints where++import TextBuilder.Prelude++{-# INLINE octalDigit #-}+octalDigit :: (Bits a, Num a) => a -> a+octalDigit a = a .&. 7 + 48++-- | Extract the first 4 bits and convert them to a Unicode codepoint of a hexadecimal digit.+-- The result is a character in the range of 0-9 or a-f.+--+-- >>> chr (fromIntegral (hexDigit 0))+-- '0'+--+-- >>> chr (fromIntegral (hexDigit 10))+-- 'a'+--+-- >>> chr (fromIntegral (hexDigit 15))+-- 'f'+--+-- The overflow is ignored, so the result is always in the range of 0-15:+-- >>> chr (fromIntegral (hexDigit 16))+-- '0'+{-# INLINE hexDigit #-}+hexDigit :: (Bits a, Num a, Ord a) => a -> a+hexDigit a = case a .&. 15 of+ a ->+ if a < 10+ then a + 48+ else a + 87
+ library/TextBuilder/Domains/Other.hs view
@@ -0,0 +1,12 @@+module TextBuilder.Domains.Other where++import qualified Data.Text as Text+import TextBuilder.Prelude+import TextBuilderCore++-- * Destructors++-- | Convert builder to string.+{-# INLINE toString #-}+toString :: TextBuilder -> String+toString = Text.unpack . toText
library/TextBuilder/Prelude.hs view
@@ -13,6 +13,10 @@ import Control.Monad.IO.Class as Exports import Control.Monad.ST as Exports import Control.Monad.ST.Unsafe as Exports+import Control.Monad.Trans.Class as Exports+import Control.Monad.Trans.Maybe as Exports hiding (liftListen, liftPass)+import Control.Monad.Trans.Reader as Exports hiding (liftCallCC, liftCatch)+import Control.Monad.Trans.State.Strict as Exports hiding (liftCallCC, liftCatch, liftListen, liftPass) import Data.Bits as Exports import Data.Bool as Exports import Data.ByteString as Exports (ByteString)@@ -40,6 +44,7 @@ import Data.Semigroup as Exports import Data.String as Exports import Data.Text as Exports (Text)+import Data.Time as Exports import Data.Traversable as Exports import Data.Tuple as Exports import Data.Unique as Exports@@ -56,6 +61,7 @@ import GHC.Generics as Exports (Generic) import GHC.IO.Exception as Exports import Numeric as Exports+import Numeric.Natural as Exports (Natural) import System.Environment as Exports import System.Exit as Exports import System.IO as Exports
test/Main.hs view
@@ -1,86 +1,153 @@ module Main where import qualified Data.ByteString as ByteString-import qualified Data.Text as A+import Data.Char (intToDigit)+import Data.Int+import Data.Proxy+import Data.String+import qualified Data.Text as Text import qualified Data.Text.Encoding as Text+import Data.Word+import Numeric+import Numeric.Natural (Natural)+import Test.QuickCheck.Classes+import Test.QuickCheck.Instances () import Test.Tasty-import Test.Tasty.HUnit import Test.Tasty.QuickCheck-import qualified TextBuilder as B-import Prelude hiding (choose)+import TextBuilder+import Util.ExtraInstances ()+import Util.TestTrees+import Prelude main :: IO ()-main =- defaultMain- $ testGroup "All tests"- $ [ testProperty "ASCII ByteString"- $ let gen = listOf $ do- list <- listOf (choose (0, 127))- return (ByteString.pack list)- in forAll gen $ \chunks ->- mconcat chunks- === Text.encodeUtf8 (B.toText (foldMap B.asciiByteString chunks)),- testProperty "Intercalation has the same effect as in Text"- $ \separator texts ->- A.intercalate separator texts- === B.toText (B.intercalate (B.text separator) (fmap B.text texts)),- testProperty "Packing a list of chars is isomorphic to appending a list of builders"- $ \chars ->- A.pack chars- === B.toText (foldMap B.char chars),- testProperty "Concatting a list of texts is isomorphic to fold-mapping with builders"- $ \texts ->- mconcat texts- === B.toText (foldMap B.text texts),- testProperty "Concatting a list of texts is isomorphic to concatting a list of builders"- $ \texts ->- mconcat texts- === B.toText (mconcat (map B.text texts)),- testProperty "Concatting a list of trimmed texts is isomorphic to concatting a list of builders"- $ \texts ->- let trimmedTexts = fmap (A.drop 3) texts- in mconcat trimmedTexts- === B.toText (mconcat (map B.text trimmedTexts)),- testProperty "Decimal" $ \(x :: Integer) ->- (fromString . show) x === (B.toText (B.decimal x)),- testProperty "Hexadecimal vs std show" $ \(x :: Integer) ->- x >= 0 ==>- (fromString . showHex x) "" === (B.toText . B.hexadecimal) x,- testCase "Separated thousands" $ do- assertEqual "" "0" (B.toText (B.thousandSeparatedUnsignedDecimal @Int ',' 0))- assertEqual "" "123" (B.toText (B.thousandSeparatedUnsignedDecimal @Int ',' 123))- assertEqual "" "1,234" (B.toText (B.thousandSeparatedUnsignedDecimal @Int ',' 1234))- assertEqual "" "1,234,567" (B.toText (B.thousandSeparatedUnsignedDecimal @Int ',' 1234567)),- testCase "Pad from left" $ do- assertEqual "" "00" (B.toText (B.padFromLeft 2 '0' ""))- assertEqual "" "00" (B.toText (B.padFromLeft 2 '0' "0"))- assertEqual "" "01" (B.toText (B.padFromLeft 2 '0' "1"))- assertEqual "" "12" (B.toText (B.padFromLeft 2 '0' "12"))- assertEqual "" "123" (B.toText (B.padFromLeft 2 '0' "123")),- testCase "Pad from right" $ do- assertEqual "" "00" (B.toText (B.padFromRight 2 '0' ""))- assertEqual "" "00" (B.toText (B.padFromRight 2 '0' "0"))- assertEqual "" "10" (B.toText (B.padFromRight 2 '0' "1"))- assertEqual "" "12" (B.toText (B.padFromRight 2 '0' "12"))- assertEqual "" "123" (B.toText (B.padFromRight 2 '0' "123"))- assertEqual "" "1 " (B.toText (B.padFromRight 3 ' ' "1")),- testCase "Hexadecimal"- $ assertEqual "" "1f23" (B.toText (B.hexadecimal @Int 0x01f23)),- testCase "Negative Hexadecimal"- $ assertEqual "" "-1f23" (B.toText (B.hexadecimal @Int (-0x01f23))),- testGroup "Time interval"- $ [ testCase "59s" $ assertEqual "" "00:00:00:59" $ B.toText $ B.intervalInSeconds @Rational 59,- testCase "minute" $ assertEqual "" "00:00:01:00" $ B.toText $ B.intervalInSeconds @Rational 60,- testCase "90s" $ assertEqual "" "00:00:01:30" $ B.toText $ B.intervalInSeconds @Rational 90,- testCase "hour" $ assertEqual "" "00:01:00:00" $ B.toText $ B.intervalInSeconds @Rational 3600,- testCase "day" $ assertEqual "" "01:00:00:00" $ B.toText $ B.intervalInSeconds @Rational 86400- ],- testCase "dataSizeInBytesInDecimal" $ do- assertEqual "" "999B" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 999))- assertEqual "" "1kB" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 1000))- assertEqual "" "1.1kB" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 1100))- assertEqual "" "1.1MB" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 1150000))- assertEqual "" "9.9MB" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 9990000))- assertEqual "" "10MB" (B.toText (B.dataSizeInBytesInDecimal @Int ',' 10100000))- assertEqual "" "1,000YB" (B.toText (B.dataSizeInBytesInDecimal @Integer ',' 1000000000000000000000000000))+main = (defaultMain . testGroup "All") tests++tests :: [TestTree]+tests =+ [ testGroup "Instances" $+ [ followsLaws $ showLaws (Proxy @TextBuilder),+ followsLaws $ eqLaws (Proxy @TextBuilder),+ followsLaws $ semigroupLaws (Proxy @TextBuilder),+ followsLaws $ monoidLaws (Proxy @TextBuilder)+ ],+ testGroup "Functions" $+ [ testGroup "decimal" $+ [ testGroup "Int" $+ [ mapsToMonoid @Int decimal,+ testProperty "Encodes the same as show" $ \(x :: Int) ->+ (fromString . show) x === toText (decimal x)+ ],+ testGroup "Int8" $+ [ mapsToMonoid @Int8 decimal,+ testProperty "Encodes the same as show" $ \(x :: Int8) ->+ (fromString . show) x === toText (decimal x)+ ],+ testGroup "Word" $+ [ mapsToMonoid @Word decimal,+ testProperty "Encodes the same as show" $ \(x :: Word) ->+ (fromString . show) x === toText (decimal x)+ ],+ testGroup "Word8" $+ [ mapsToMonoid @Word8 decimal,+ testProperty "Encodes the same as show" $ \(x :: Word8) ->+ (fromString . show) x === toText (decimal x)+ ],+ testGroup "Integer" $+ [ mapsToMonoid @Integer decimal,+ testProperty "Encodes the same as show" $ \(x :: Integer) ->+ (fromString . show) x === toText (decimal x)+ ],+ testGroup "Natural" $+ [ mapsToMonoid @Natural decimal,+ testProperty "Encodes the same as show" $ \(x :: Natural) ->+ (fromString . show) x === toText (decimal x)+ ]+ ],+ testGroup "fixedLengthDecimal" $+ [ testGroup "Word" $+ [ mapsToMonoid @Word (fixedLengthDecimal 42)+ ],+ testGroup "Natural" $+ [ mapsToMonoid @Natural (fixedLengthDecimal 42),+ testProperty "Encodes the same as printf" $ \(size :: Word8, val :: Natural) ->+ let rendered = show val+ renderedLength = length rendered+ intSize = fromIntegral size+ padded =+ if renderedLength > intSize+ then drop (renderedLength - intSize) rendered+ else replicate (intSize - renderedLength) '0' <> rendered+ in fromString padded+ === toText (fixedLengthDecimal (fromIntegral size) val)+ ]+ ],+ testGroup "thousandSeparatedDecimal" $+ [ testGroup "Int" $+ [ mapsToMonoid @Int (thousandSeparatedDecimal ','),+ testProperty "Encodes the same as show" $ \(x :: Int) ->+ (fromString . show) x === Text.filter (/= ',') (toText (thousandSeparatedDecimal ',' x))+ ],+ testGroup "Int8" $+ [ mapsToMonoid @Int8 (thousandSeparatedDecimal ','),+ testProperty "Encodes the same as show" $ \(x :: Int8) ->+ (fromString . show) x === Text.filter (/= ',') (toText (thousandSeparatedDecimal ',' x))+ ],+ testGroup "Integer" $+ [ mapsToMonoid @Integer (thousandSeparatedDecimal ','),+ testProperty "Encodes the same as show" $ \(x :: Integer) ->+ (fromString . show) x === Text.filter (/= ',') (toText (thousandSeparatedDecimal ',' x))+ ]+ ],+ testGroup "binary" $+ [ testGroup "Int" $+ [ mapsToMonoid @Int binary,+ testProperty "Encodes the same as showIntAtBase" $ \(x :: Word32) ->+ (Text.justifyRight 32 '0' . Text.pack . showIntAtBase 2 intToDigit x) "" === toText (binary x)+ ]+ ],+ testGroup "octal" $+ [ testGroup "Int" $+ [ mapsToMonoid @Int octal,+ testProperty "Encodes the same as showIntAtBase" $ \(x :: Word32) ->+ (Text.justifyRight 11 '0' . Text.pack . showIntAtBase 8 intToDigit x) "" === toText (octal x)+ ]+ ],+ testGroup "hexadecimal" $+ [ testGroup "Int" $+ [ mapsToMonoid @Int hexadecimal,+ testProperty "Encodes the same as showHex" $ \(x :: Word32) ->+ (Text.justifyRight 8 '0' . Text.pack . showHex x) "" === toText (hexadecimal x)+ ]+ ],+ testGroup "unsafeUtf8ByteString" $+ [ mapsToMonoid (unsafeUtf8ByteString . Text.encodeUtf8),+ testProperty "Works on ASCII" $+ let gen = listOf do+ list <- listOf (choose (0, 127))+ return (ByteString.pack list)+ in forAll gen \chunks ->+ mconcat chunks+ === Text.encodeUtf8 (toText (foldMap unsafeUtf8ByteString chunks))+ ],+ testGroup "intercalate" $+ [ customGenMonoid do+ sep <- arbitrary+ texts <- listOf arbitrary+ return (intercalate (text sep) (fmap text texts)),+ testProperty "Has the same effect as in Text" $+ \separator texts ->+ Text.intercalate separator texts+ === toText (intercalate (text separator) (fmap text texts))+ ],+ testGroup "intercalateMap" $+ [ customGenMonoid do+ sep <- arbitrary+ texts <- listOf arbitrary+ return (intercalateMap (text sep) text texts),+ testProperty "intercalateMap sep mapper == intercalate sep . fmap mapper" $+ \separator ints ->+ Text.intercalate separator (fmap (fromString . show @Int) ints)+ === toText (intercalateMap (text separator) decimal ints)+ ] ]+ ]
+ test/Util/ExtraInstances.hs view
@@ -0,0 +1,12 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module Util.ExtraInstances where++import qualified Data.Text.Lazy.Builder as TextLazyBuilder+import Test.QuickCheck+import Test.QuickCheck.Instances ()+import Prelude++instance Arbitrary TextLazyBuilder.Builder where+ arbitrary =+ TextLazyBuilder.fromLazyText <$> arbitrary
+ test/Util/TestTrees.hs view
@@ -0,0 +1,86 @@+module Util.TestTrees where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Monoid+import Data.Semigroup+import Test.QuickCheck+import Test.QuickCheck.Classes+import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.QuickCheck+import Prelude++-- | Tests mapping from @a@ to @b@ to produce a valid 'Monoid'.+--+-- Tests the following properties:+--+-- [/Associative/]+-- @a '<>' (b '<>' c) ≡ (a '<>' b) '<>' c@+-- [/Semigroup Concatenation/]+-- @'sconcat' as ≡ 'foldr1' ('<>') as@+-- [/Times/]+-- @'stimes' n a ≡ 'foldr1' ('<>') ('replicate' n a)@+-- [/Left Identity/]+-- @mappend mempty a ≡ a@+-- [/Right Identity/]+-- @mappend a mempty ≡ a@+-- [/Monoid Concatenation/]+-- @mconcat as ≡ foldr mappend mempty as@+mapsToMonoid ::+ forall a b.+ (Arbitrary a, Show a, Eq a, Monoid b, Eq b, Show b) =>+ -- | Embed in monoid.+ (a -> b) ->+ TestTree+mapsToMonoid embed =+ customGenMonoid (embed <$> arbitrary)++-- | Tests mapping from @a@ to @b@ to produce a valid 'Monoid'.+--+-- Tests the following properties:+--+-- [/Associative/]+-- @a '<>' (b '<>' c) ≡ (a '<>' b) '<>' c@+-- [/Semigroup Concatenation/]+-- @'sconcat' as ≡ 'foldr1' ('<>') as@+-- [/Times/]+-- @'stimes' n a ≡ 'foldr1' ('<>') ('replicate' n a)@+-- [/Left Identity/]+-- @mappend mempty a ≡ a@+-- [/Right Identity/]+-- @mappend a mempty ≡ a@+-- [/Monoid Concatenation/]+-- @mconcat as ≡ foldr mappend mempty as@+customGenMonoid ::+ (Monoid a, Eq a, Show a) =>+ Gen a ->+ TestTree+customGenMonoid gen =+ testGroup+ "Monoid"+ [ testProperty "Is associative" do+ x <- gen+ y <- gen+ z <- gen+ pure (x <> (y <> z) === (x <> y) <> z),+ testProperty "Semigroup concatenation" do+ xs <- (:|) <$> gen <*> listOf gen+ pure (sconcat xs === foldr1 (<>) xs),+ testProperty "Times" do+ x <- gen+ Positive n <- arbitrary+ pure (stimes n x === foldr1 (<>) (replicate n x)),+ testProperty "Left identity" do+ x <- gen+ pure (mempty <> x === x),+ testProperty "Right identity" do+ x <- gen+ pure (x <> mempty === x),+ testProperty "Monoid concatenation" do+ xs <- listOf gen+ pure (mconcat xs === foldr mappend mempty xs)+ ]++followsLaws :: Laws -> TestTree+followsLaws Laws {..} =+ testProperties lawsTypeclass lawsProperties
text-builder.cabal view
@@ -1,12 +1,39 @@ cabal-version: 3.0 name: text-builder-version: 0.6.10+version: 1.0.0.5 category: Text, Builders-synopsis: Efficient strict text builder+synopsis: Efficient and flexible strict text builder description:- - Efficient monoidal builder of strict textual values- - Text formatting library, an alternative to `printf` or the "formatting" Haskell library+ = Summary + Fast strict text builder and simple type-safe formatting library.++ == The Builder++ The builder abstraction provided by this library is much faster than the standard lazy @Builder@ and even the recently introduced @StrictTextBuilder@ from \"text\". Benchmarks are distributed with the source code. You can see the results in the README file.++ The abstraction constructs text in two phases. In the first one it estimates the size of the byte array and in the second one it allocates it once and populates it in one go.++ == Simple and type-safe formatting library++ The monoidal API of the library provides a simple yet type-safe alternative to formatting strings via @printf@-like tools or more involved solutions like the popular \"[formatting](https://hackage.haskell.org/package/formatting)\" library.++ == Quality++ Every bit of the library is heavily covered with tests with CI running tests on a variety of versions of GHC and the \"text\" library. This is crucial because the \"text\" library has made a switch from UTF-16 to UTF-8, leading to drastic changes in its low-level constructs, which builder libraries must rely on, and this library supports both versions of \"text\".++ = Ecosystem++ Following is a list of libraries that, alongside this one, make an integrated ecosystem:++ - "[text-builder-time](https://hackage.haskell.org/package/text-builder-time)" - formatters for the "time" library++ - "[text-builder-core](https://hackage.haskell.org/package/text-builder-core)" - lower-level unsafe constructs for implementing efficient formatters compatible with this library++ - "[text-builder-dev](https://hackage.haskell.org/package/text-builder-dev)" - edge of development of new features providing a richer functionality at the cost of more frequent major releases++ - "[text-builder-lawful-conversions](https://hackage.haskell.org/package/text-builder-lawful-conversions)" - integration with the \"lawful-conversions\" library, providing bidirectional conversions with various textual and builder types.+ homepage: https://github.com/nikita-volkov/text-builder bug-reports: https://github.com/nikita-volkov/text-builder/issues author: Nikita Volkov <nikita.y.volkov@mail.ru>@@ -14,8 +41,9 @@ copyright: (c) 2017, Nikita Volkov license: MIT license-file: LICENSE-extra-source-files:+extra-doc-files: CHANGELOG.md+ LICENSE README.md source-repository head@@ -35,6 +63,7 @@ DeriveFunctor DeriveGeneric DeriveTraversable+ DerivingStrategies EmptyDataDecls FlexibleContexts FlexibleInstances@@ -57,37 +86,53 @@ RecordWildCards ScopedTypeVariables StandaloneDeriving+ StrictData TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators UnboxedTuples+ ViewPatterns library import: base hs-source-dirs: library- exposed-modules:- Text.Builder- TextBuilder+ exposed-modules: TextBuilder+ other-modules:+ TextBuilder.Domains.ByteString+ TextBuilder.Domains.Combinators+ TextBuilder.Domains.Digits+ TextBuilder.Domains.Digits.Codepoints+ TextBuilder.Domains.Other+ TextBuilder.Prelude - other-modules: TextBuilder.Prelude build-depends: base >=4.11 && <5, bytestring >=0.10 && <0.13, text >=1.2 && <3,- text-builder-dev >=0.3.10 && <0.4,+ text-builder-core ^>=0.1.1.1,+ time >=1.12 && <2,+ transformers >=0.5 && <0.7, test-suite test import: base type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Main.hs+ other-modules:+ Util.ExtraInstances+ Util.TestTrees+ build-depends:- rerebase <2,- tasty >=1.2.3 && <2,- tasty-hunit >=0.10.0.2 && <0.11,- tasty-quickcheck >=0.10.1 && <0.12,+ QuickCheck >=2.14 && <3,+ base >=4.11 && <5,+ bytestring >=0.10 && <0.13,+ quickcheck-classes >=0.6.5 && <0.7,+ quickcheck-instances >=0.3.32 && <0.4,+ tasty >=1.5.3 && <2,+ tasty-quickcheck >=0.11.1 && <0.12,+ text >=1.2 && <3, text-builder, benchmark bench@@ -98,10 +143,14 @@ -O2 -threaded -with-rtsopts=-N- -funbox-strict-fields+ -with-rtsopts=-A32m+ -with-rtsopts=-T+ -fproc-alignment=64 main-is: Main.hs build-depends:- criterion >=1.5.6.1 && <2,- rerebase >=1 && <2,+ base >=4.11 && <5,+ tasty-bench ^>=0.5,+ text >=2.1.2 && <3, text-builder,+ text-builder-linear ^>=0.1.3,