commit 21ce81509047bcf3652841534851ad83af47aa74
parent f9d887c56b669c27203e5394c9ad05736a5ff2d8
Author: Jared Tobin <jared@jtobin.io>
Date: Fri, 20 Sep 2019 09:32:23 -0230
co: add patq support
Adds a Patq type, plus analogues of all the patp functions for dealing
with it (patq, fromPatq, renderPatq, parsePatq).
Diffstat:
5 files changed, 220 insertions(+), 69 deletions(-)
diff --git a/README.md b/README.md
@@ -8,22 +8,24 @@ Utilities for phonetic base wrangling.
## What
-Here you can primarily find functions for dealing with the "patp" *phonetic
-base* used by Urbit. The `@p` encoding is used for naming ships; it uniquely
-represents a nonnegative integer (i.e. an *atom*) in a memorable and
-pronounceable fashion.
+Here you can primarily find functions for dealing with the "patp" and "patq"
+*phonetic bases* used by Urbit. The `@p` encoding is used for naming ships,
+whereas the `@q` encoding is used for arbitrary data; they both uniquely
+represent nonnegative integers (i.e. *atoms*) in a memorable and pronounceable
+fashion.
The `@p` encoding is an *obfuscated* representation of an underlying atom, in
particular, hence the 'ob' in the library's name.
## Usage
-The library exposes two functions, `patp` and `fromPatp`, for converting
-between representations. You can render a `patp` value via the `render`
-function, and parse one from Text via `parse`.
+The library exposes two families of functions, `patp` and `fromPatp`, and then
+`patq` and `fromPatq`, for converting between representations appropriately.
+You can render `{patp, patq}` values via the `render{Patp, Patq}` functions,
+and parse them from Text via `parse{Patp, Patq}` respectively.
-The `clan` and `sein` functions, for determining a ship's class and (default)
-parent, are also exposed.
+The useful `clan` and `sein` functions, for determining a ship's class and
+(default) parent, are also exposed.
Here are some quick examples:
@@ -31,11 +33,14 @@ Here are some quick examples:
> :set -XOverloadedStrings
> import qualified Urbit.Ob as Ob
> let nidsut = Ob.patp 15663360
-> Ob.render nidsut
+> let marzod = Ob.patq (Ob.fromPatp nidsut)
+> Ob.renderPatp nidsut
"~nidsut-tomdun"
+> Ob.renderPatq marzod
+"~mun-marzod"
> Ob.fromPatp nidsut
15663360
-> Ob.parse "~nidsut-tomdun"
+> Ob.parsePatp "~nidsut-tomdun"
Right ~nidsut-tomdun
> Ob.clan nidsut
Planet
diff --git a/lib/Urbit/Ob/Co.hs b/lib/Urbit/Ob/Co.hs
@@ -16,12 +16,19 @@
module Urbit.Ob.Co (
Patp
+ , Patq
, patp
+ , patq
+
, fromPatp
+ , fromPatq
+
+ , renderPatp
+ , renderPatq
- , render
- , parse
+ , parsePatp
+ , parsePatq
) where
import qualified Data.ByteString as BS
@@ -46,14 +53,27 @@ import qualified Urbit.Ob.Ob as Ob (fein, fynd)
--
-- (It's also used for naming comets, i.e. self-signed 128-bit Urbit ships.)
--
-newtype Patp = Patp BS.ByteString
- deriving (Eq, Generic)
+newtype Patp = Patp {
+ unPatp :: BS.ByteString
+ } deriving (Eq, Generic)
instance Show Patp where
- show = T.unpack . render
+ show = T.unpack . renderPatp
+
+-- | Hoon's \@q encoding.
+--
+-- Unlike \@p, the \@q encoding is a /non-obfuscated/ representation of an
+-- atom.
+--
+-- It's typically used for serializing arbitrary data in a memorable and
+-- pronounceable fashion.
+--
+newtype Patq = Patq {
+ unPatq :: BS.ByteString
+ } deriving (Eq, Generic)
-unPatp :: Patp -> BS.ByteString
-unPatp (Patp p) = p
+instance Show Patq where
+ show = T.unpack . renderPatq
-- | Convert a 'Natural' to \@p.
--
@@ -69,6 +89,20 @@ unPatp (Patp p) = p
patp :: Natural -> Patp
patp = Patp . BS.reverse . C.unroll . Ob.fein
+-- | Convert a 'Natural' to \@q.
+--
+-- >>> patq 0
+-- ~zod
+-- >>> patp 256
+-- ~marzod
+-- >>> patp 65536
+-- ~nec-dozzod
+-- >>> patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+-- ~fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes
+--
+patq :: Natural -> Patq
+patq = Patq . BS.reverse . C.unroll
+
-- | Convert a \@p value to its corresponding 'Natural'.
--
-- >>> let zod = patp 0
@@ -78,17 +112,68 @@ patp = Patp . BS.reverse . C.unroll . Ob.fein
fromPatp :: Patp -> Natural
fromPatp = Ob.fynd . C.roll . BS.reverse . unPatp
+-- | Convert a \@q value to its corresponding 'Natural'.
+--
+-- >>> let zod = patq 0
+-- >>> fromPatq zod
+-- 0
+--
+fromPatq :: Patq -> Natural
+fromPatq = C.roll . BS.reverse . unPatq
+
-- | Render a \@p value as 'T.Text'.
--
--- >>> render (patp 0)
+-- >>> renderPatp (patp 0)
-- "~zod"
--- >>> render (patp 15663360)
+-- >>> renderPatp (patp 15663360)
-- "~nidsut-tomdun"
-render :: Patp -> T.Text
-render (Patp bs) = render' bs
+renderPatp :: Patp -> T.Text
+renderPatp (Patp bs) = render' Padding LongSpacing bs
-render' :: BS.ByteString -> T.Text
-render' bs =
+-- | Render a \@p value as 'T.Text'.
+--
+-- >>> renderPatq (patq 0)
+-- "~zod"
+-- >>> renderPatq (patq 15663360)
+-- "~mun-marzod"
+renderPatq :: Patq -> T.Text
+renderPatq (Patq bs) = render' NoPadding ShortSpacing bs
+
+-- | Parse a \@p value existing as 'T.Text'.
+--
+-- >>> parsePatp "~nidsut-tomdun"
+-- Right ~nidsut-tomdun
+-- > parsePatp "~fipfes-fipfes-fipfes-doznec"
+-- Right ~fipfes-fipfes-fipfes-doznec
+--
+parsePatp :: T.Text -> Either T.Text Patp
+parsePatp = fmap Patp . parse
+
+-- | Parse a \@q value existing as 'T.Text'.
+--
+-- >>> parsePatq "~nec-dozzod"
+-- Right ~nec-dozzod
+-- > parsePatq "~fipfes-fipfes-fipfes-doznec"
+-- Right ~fipfes-fipfes-fipfes-doznec
+--
+parsePatq :: T.Text -> Either T.Text Patq
+parsePatq = fmap Patq . parse
+
+-- Padding option for rendering.
+data Padding =
+ Padding
+ | NoPadding
+ deriving Eq
+
+-- Spacing option for rendering.
+data Spacing =
+ LongSpacing
+ | ShortSpacing
+ deriving Eq
+
+-- General-purpose renderer.
+render' :: Padding -> Spacing -> BS.ByteString -> T.Text
+render' padding spacing bs =
T.cons '~'
. snd
. BS.foldr alg (0 :: Int, mempty)
@@ -97,27 +182,29 @@ render' bs =
alg val (idx, acc) =
let syl = if even idx then suffix val else prefix val
glue
- | idx `mod` 8 == 0 = if idx == 0 then mempty else "--"
+ | idx `mod` 8 == 0 = if idx == 0 then mempty else dash
| even idx = "-"
| otherwise = mempty
in (succ idx, syl <> glue <> acc)
- padded =
+ padded
+ | padCondition = BS.cons 0 bs
+ | otherwise = bs
+
+ dash = case spacing of
+ LongSpacing -> "--"
+ ShortSpacing -> "-"
+
+ padCondition =
let len = BS.length bs
- in if (odd len && len > 2) || len == 0
- then BS.cons 0 bs
- else bs
+ in case padding of
+ NoPadding -> len == 0
+ Padding -> (odd len && len > 2) || len == 0
--- | Parse a \@p value existing as 'T.Text'.
---
--- >>> parse "~nidsut-tomdun"
--- Right ~nidsut-tomdun
--- > parse "~fipfes-fipfes-fipfes-doznec"
--- Right ~fipfes-fipfes-fipfes-doznec
---
-parse :: T.Text -> Either T.Text Patp
+-- General-purpose (non-strict) parser.
+parse :: T.Text -> Either T.Text BS.ByteString
parse p =
- fmap (Patp . snd)
+ fmap snd
$ foldrM alg (0 :: Int, mempty) syls
where
alg syl (idx, acc) = do
diff --git a/test/Co/Tests/Property.hs b/test/Co/Tests/Property.hs
@@ -4,7 +4,7 @@ module Co.Tests.Property (
) where
import qualified Data.Text as T
-import Data.Word (Word32)
+import Data.Word (Word64)
import Numeric.Natural (Natural)
import Test.Hspec
import Test.Hspec.Core.QuickCheck (modifyMaxSuccess)
@@ -12,13 +12,19 @@ import Test.QuickCheck
import qualified Urbit.Ob.Co as Co
nats :: Gen Natural
-nats = fmap fromIntegral (arbitrary :: Gen Word32)
+nats = fmap fromIntegral (arbitrary :: Gen Word64)
patps :: Gen Co.Patp
patps = fmap Co.patp nats
+patqs :: Gen Co.Patq
+patqs = fmap Co.patq nats
+
patpStrings :: Gen T.Text
-patpStrings = fmap Co.render patps
+patpStrings = fmap Co.renderPatp patps
+
+patqStrings :: Gen T.Text
+patqStrings = fmap Co.renderPatq patqs
tests :: Spec
tests = do
@@ -32,11 +38,29 @@ tests = do
it "inverts fromPatp" $
forAll patps $ \x -> Co.patp (Co.fromPatp x) == x
- describe "render" $
+ describe "renderPatp" $
modifyMaxSuccess (const 1000) $
- it "inverts parse" $
+ it "inverts parsePatp" $
forAll patpStrings $ \x ->
- case Co.parse x of
+ case Co.parsePatp x of
+ Left _ -> False
+ Right p -> Co.renderPatp p == x
+
+ describe "fromPatq" $
+ modifyMaxSuccess (const 1000) $
+ it "inverts patq" $
+ forAll nats $ \x -> Co.fromPatq (Co.patq x) == x
+
+ describe "patq" $
+ modifyMaxSuccess (const 1000) $
+ it "inverts fromPatq" $
+ forAll patqs $ \x -> Co.patq (Co.fromPatq x) == x
+
+ describe "renderPatq" $
+ modifyMaxSuccess (const 1000) $
+ it "inverts parsePatq" $
+ forAll patqStrings $ \x ->
+ case Co.parsePatq x of
Left _ -> False
- Right p -> Co.render p == x
+ Right p -> Co.renderPatq p == x
diff --git a/test/Co/Tests/Unit.hs b/test/Co/Tests/Unit.hs
@@ -9,32 +9,61 @@ import Test.Hspec
import qualified Urbit.Ob.Co as Co
tests :: Spec
-tests =
- describe "render" $ do
+tests = do
+ describe "renderPatp" $ do
it "matches 32-bit reference values" $ do
- Co.render (Co.patp 0) `shouldBe` "~zod"
- Co.render (Co.patp 255) `shouldBe` "~fes"
- Co.render (Co.patp 256) `shouldBe` "~marzod"
- Co.render (Co.patp 65535) `shouldBe` "~fipfes"
- Co.render (Co.patp 65536) `shouldBe` "~dapnep-ronmyl"
- Co.render (Co.patp 15663360) `shouldBe` "~nidsut-tomdun"
- Co.render (Co.patp 0xFFFFFFFF) `shouldBe` "~dostec-risfen"
+ Co.renderPatp (Co.patp 0) `shouldBe` "~zod"
+ Co.renderPatp (Co.patp 255) `shouldBe` "~fes"
+ Co.renderPatp (Co.patp 256) `shouldBe` "~marzod"
+ Co.renderPatp (Co.patp 65535) `shouldBe` "~fipfes"
+ Co.renderPatp (Co.patp 65536) `shouldBe` "~dapnep-ronmyl"
+ Co.renderPatp (Co.patp 15663360) `shouldBe` "~nidsut-tomdun"
+ Co.renderPatp (Co.patp 0xFFFFFFFF) `shouldBe` "~dostec-risfen"
it "matches 64-bit reference values" $ do
let big_64_01 = 0x0000000100000000
- Co.render (Co.patp big_64_01) `shouldBe` "~doznec-dozzod-dozzod"
+ Co.renderPatp (Co.patp big_64_01) `shouldBe` "~doznec-dozzod-dozzod"
let big_64_02 = 0xFFFFFFFFFFFFFFFF
- Co.render (Co.patp big_64_02) `shouldBe` "~fipfes-fipfes-dostec-risfen"
+ Co.renderPatp (Co.patp big_64_02) `shouldBe` "~fipfes-fipfes-dostec-risfen"
it "matches 128-bit reference values" $ do
let big_128_01 = 0x00000000000000010000000000000000
patp_128_01 = "~doznec--dozzod-dozzod-dozzod-dozzod"
- Co.render (Co.patp big_128_01) `shouldBe` patp_128_01
+ Co.renderPatp (Co.patp big_128_01) `shouldBe` patp_128_01
let big_128_02 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
patp_128_02 =
"~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes"
- Co.render (Co.patp big_128_02) `shouldBe` patp_128_02
+ Co.renderPatp (Co.patp big_128_02) `shouldBe` patp_128_02
+
+ describe "renderPatq" $ do
+ it "matches 32-bit reference values" $ do
+ Co.renderPatq (Co.patq 0) `shouldBe` "~zod"
+ Co.renderPatq (Co.patq 255) `shouldBe` "~fes"
+ Co.renderPatq (Co.patq 256) `shouldBe` "~marzod"
+ Co.renderPatq (Co.patq 65535) `shouldBe` "~fipfes"
+ Co.renderPatq (Co.patq 65536) `shouldBe` "~nec-dozzod"
+ Co.renderPatq (Co.patq 15663360) `shouldBe` "~mun-marzod"
+ Co.renderPatq (Co.patq 0xFFFFFFFF) `shouldBe` "~fipfes-fipfes"
+
+ it "matches 64-bit reference values" $ do
+ let big_64_01 = 0x0000000100000000
+ Co.renderPatq (Co.patq big_64_01) `shouldBe` "~nec-dozzod-dozzod"
+
+ let big_64_02 = 0xFFFFFFFFFFFFFFFF
+ patq_64_02 = "~fipfes-fipfes-fipfes-fipfes"
+ Co.renderPatq (Co.patq big_64_02) `shouldBe` patq_64_02
+
+ it "matches 128-bit reference values" $ do
+ let big_128_01 = 0x00000000000000010000000000000000
+ patq_128_01 = "~nec-dozzod-dozzod-dozzod-dozzod"
+ Co.renderPatq (Co.patq big_128_01) `shouldBe` patq_128_01
+
+ let big_128_02 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ patq_128_02 =
+ "~fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes"
+
+ Co.renderPatq (Co.patq big_128_02) `shouldBe` patq_128_02
diff --git a/urbit-hob.cabal b/urbit-hob.cabal
@@ -12,16 +12,20 @@ license-file: LICENSE
build-type: Simple
cabal-version: >= 1.10
description:
- Here you can primarily find functions for dealing with the \"patp\" phonetic
- base used by Urbit. The \@p encoding is used for naming ships; it uniquely
- represents a 32-bit number in a memorable and pronounceable fashion.
+ Here you can primarily find functions for dealing with the \"patp\" and
+ \"patq\" /phonetic bases/ used by Urbit. The \@p encoding is used for naming
+ ships, while the \@q encoding is used for arbitrary data; they each uniquely
+ represent an underlying natural number (or /atom/) in a memorable and
+ pronounceable fashion.
.
- The \@p encoding is an /obfuscated/ representation of an underlying 32-bit
- number, in particular, hence the \"ob\" in the library's name.
+ The \@p encoding is an /obfuscated/ representation of an underlying atom, in
+ particular, hence the \"ob\" in the library's name.
.
- The @Urbit.Ob@ module exposes two functions, 'patp' and 'fromPatp', for
- converting between representations. You can also render a 'Patp' value via
- the 'render' function, or parse one via 'parse'.
+ The @Urbit.Ob@ module exposes two families of functions, 'patp' and
+ 'fromPatp', and then 'patq' and 'fromPatq', for converting between
+ representations. You can also render a 'Patp' or 'Patq' value as 'Text' via
+ the 'renderPatp' and 'renderPatq' functions, or parse them from 'Text' via
+ 'parsePatp' and 'parsePatq'.
.
Since \@p values represent ships, some utilities for dealing with ships are
also exposed. The 'clan' and 'sein' functions are useful for determining a
@@ -31,13 +35,15 @@ description:
.
>>> :set -XOverloadedStrings
>>> import qualified Urbit.Ob as Ob
- >>>
>>> let nidsut = Ob.patp 15663360
- >>> Ob.render nidsut
+ >>> let marzod = Ob.patq (Ob.fromPatp nidsut)
+ >>> Ob.renderPatp nidsut
"~nidsut-tomdun"
+ >>> Ob.renderPatq marzod
+ "~mun-marzod"
>>> Ob.fromPatp nidsut
15663360
- >>> Ob.parse "~nidsut-tomdun"
+ >>> Ob.parsePatp "~nidsut-tomdun"
Right ~nidsut-tomdun
>>> Ob.clan nidsut
Planet