--
-- Human exchangable identifiers and locators
--
-- Copyright © 2011-2018 Operational Dynamics Consulting, Pty Ltd
--
-- The code in this file, and the program it is a part of, is
-- made available to you by its authors as open source software:
-- you can redistribute it and/or modify it under the terms of
-- the BSD licence.
--
-- This code originally licenced GPLv2. Relicenced BSD3 on 2 Jan 2014.
--
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE TypeApplications #-}
module Data.Locator.Common
( Locator(..)
, represent
, value
, toLocatorUnique
, multiply
, fromLocator
, concatToInteger
, digest
) where
import Prelude hiding (toInteger)
import Crypto.Hash as Crypto
import qualified Data.ByteArray as B
import qualified Data.ByteString.Char8 as S
import Data.List (mapAccumL)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Word
class (Ord α, Enum α, Bounded α) => Locator α where
locatorToDigit :: α -> Char
digitToLocator :: Char -> α
represent :: Locator α => α -> Int -> Char
represent (_ :: α) n =
locatorToDigit $ (toEnum n :: α)
{-
value :: Locator α => α -> Char -> Int
value c (_ :: α) =
fromEnum $ (digitToLocator c :: α)
-}
value :: Locator α => α -> Char -> Int
value (_ :: α) c =
fromEnum $ (digitToLocator c :: α)
--
-- | Represent a number in Locator16a format. This uses the Locator16 symbol
-- set, and additionally specifies that no symbol can be repeated. The /a/ in
-- Locator16a represents that this transformation is done on the cheap; when
-- converting if we end up with \'9\' \'9\' we simply pick the subsequent digit
-- in the enum, in this case getting you \'9\' \'K\'.
--
-- Note that the transformation is /not/ reversible. A number like @4369@
-- (which is @0x1111@, incidentally) encodes as @12C4@. So do @4370@, @4371@,
-- and @4372@. The point is not uniqueness, but readibility in adverse
-- conditions. So while you can count locators, they don't map continuously to
-- base10 integers.
--
-- The first argument is the number of digits you'd like in the locator; if the
-- number passed in is less than 16^limit, then the result will be padded.
--
-- >>> toLocator16a 6 4369
-- 12C40F
--
toLocatorUnique :: Locator α => Int -> Int -> α -> String
toLocatorUnique limit n (_ :: α) =
let
n' = abs n
ls = convert n' (replicate limit (minBound @α))
(_,us) = mapAccumL uniq Set.empty ls
in
map locatorToDigit (take limit us)
where
convert :: Locator α => Int -> [α] -> [α]
convert 0 xs = xs
convert i xs =
let
(d,r) = divMod i 16
x = toEnum r
in
convert d (x:xs)
uniq :: Locator α => Set α -> α -> (Set α, α)
uniq s x =
if Set.member x s
then uniq s (subsequent x)
else (Set.insert x s, x)
subsequent :: Locator α => α -> α
subsequent x =
if x == maxBound
then minBound
else succ x
multiply :: Locator α => α -> Int -> Char -> Int
multiply (locator :: a) acc c =
let
base = fromEnum (maxBound @a) + 1
in
(acc * base) + (value locator c)
--
-- | Given a number encoded as a Locator, convert it back to an integer.
--
fromLocator :: Locator α => α -> String -> Int
fromLocator locator ss =
foldl (multiply locator) 0 ss
--
-- Given a string, convert it into a N character hash.
--
concatToInteger :: [Word8] -> Int
concatToInteger bytes =
foldl fn 0 bytes
where
fn acc b = (acc * 256) + (fromIntegral b)
digest :: String -> Int
digest ws =
i
where
i = concatToInteger h
h = B.unpack h'
h' = Crypto.hash x' :: Crypto.Digest Crypto.SHA1
x' = S.pack ws