/ThirtyTo

Encode, Decode and Generate Random String in Base32Crockford Format

Primary LanguageSwiftMIT LicenseMIT

ThirtyTo

ThirtyTo

Swift Package for using Base32Crockford Encoding for Data and Identifiers.

SwiftPM Twitter GitHub GitHub issues GitHub Workflow Status

Codecov CodeFactor Grade codebeat badge Code Climate maintainability Code Climate technical debt Code Climate issues Reviewed by Hound

Table of Contents

Introduction

ThirtyTo provides a way to encode data and create identifiers which is both efficient and human-readable. While Base64 is more efficient it is not very human-readable with both both upper case and lower case letters as well as punctuation.

Requirements

Apple Platforms

  • Xcode 13.3 or later
  • Swift 5.5.2 or later
  • iOS 14 / watchOS 6 / tvOS 14 / macOS 12 or later deployment targets

Linux

  • Ubuntu 18.04 or later
  • Swift 5.5.2 or later

Installation

Use the Swift Package Manager to install this library via the repository url:

https://github.com/brightdigit/ThirtyTo.git

Use version up to 1.0.

Why use Base32Crockford

Base32Crockford offers the most reasonable compromise when it comes to encoding data. Being a super set of Base16, it uses all ten digits and 22 of the 26 Latin upper case characters.

Symbol Value Decode Symbol Encode Symbol
0 0 O o 0
1 1 I i L l 1
2 2 2
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 A a A
11 B b B
12 C c C
13 D d D
14 E e E
15 F f F
16 G g G
17 H h H
18 J j J
19 K k K
20 M m M
21 N n N
22 P p P
23 Q q Q
24 R r R
25 S s S
26 T t T
27 V v V
28 W w W
29 X x X
30 Y y Y
31 Z z Z

Usage

ThirtyTo enables the encoding and decoding data in Base32Crockford as well as creation of unique identifiers. There are a variety of options available for encoding and decoding.

Encoding and Decoding Data

All encoding and decoding is done through the Base32CrockfordEncoding.encoding object. This provides an interface into encoding and decoding data as well standardizing.

To encode any data call:

public func encode(
    data: Data,
    options: Base32CrockfordEncodingOptions = .none
  ) -> String

Therefore to encode a Data object, simply call via:

let data : Data // 0x00b003786a8d4aa28411f4e268c43629 
let base32String = Base32CrockfordEncoding.encoding.encode(data: data)
print(base32String) // P01QGTMD9AH884FMW9MC8DH9

If you wish to decode the string you can call Base32CrockfordEncoding.decode:

let data = try Base32CrockfordEncoding.encoding.decode(
    base32Encoded: "P01QGTMD9AH884FMW9MC8DH9"
) // 0x00b003786a8d4aa28411f4e268c43629

The Base32CrockfordEncoding.encode object provides the ability to specify options on formatting and a checksum.

How Checksum Works

You can optionally provide a checksum character at the end which allows for detecting transmission and entry errors early and inexpensively.

According to the specification:

The check symbol encodes the number modulo 37, 37 being the least prime number greater than 32. We introduce 5 additional symbols that are used only for encoding or decoding the check symbol.

The additional 5 symbols are:

Symbol Value Decode Symbol Encode Symbol
32 * *
33 ~ ~
34 $ $
35 = =
36 U u U

If you wish to include the checksum, pass true for the withChecksum property on the Base32CrockfordEncodingOptions object:

let data : Data // 0xb63d798c4329401684d1d41d3becc95c
let base32String = Base32CrockfordEncoding.encoding.encode(
    data: data,
    options: .init(withChecksum: true)
)
print(base32String) // 5P7NWRRGS980B89MEM3MXYSJAW5

When decoding a string wtih a checksum, you must specify true for the withChecksum property on Base32CrockfordDecodingOptions:

let data = try Base32CrockfordEncoding.encoding.decode(
    base32Encoded: "5P7NWRRGS980B89MEM3MXYSJAW5"a,
    options: .init(withChecksum: true)
) // 0xb63d798c4329401684d1d41d3becc95c

Besides adding a checksum, Base32CrockfordEncodingOptions also provides the ability to add a grouping separator.

Using Group Separators

According to the Base32Crockford specification:

Hyphens (-) can be inserted into symbol strings. This can partition a string into manageable pieces, improving readability by helping to prevent confusion. Hyphens are ignored during decoding.

To insert hyphens to the encoded string, provide the GroupingOptions object to Base32CrockfordEncodingOptions:

let data : Data // 00c9c37484b85a42e8b3e7fbf806f2661b
let base32StringGroupedBy3 = Base32CrockfordEncoding.encoding.encode(
    data: data,
    options: .init(groupingBy: .init(maxLength: 3))
)
let base32StringGroupedBy9 = Base32CrockfordEncoding.encoding.encode(
    data: data,
    options: .init(groupingBy: .init(maxLength: 9))
)
print(base32StringGroupedBy3) // 69R-DT8-9E2-T8B-MB7-SZV-Z03-F4S-GV2
print(base32StringGroupedBy9) // 69RDT89E2-T8BMB7SZV-Z03F4SGV2

When decoding, hyphens are ignored:

let dataNoHyphens = try Base32CrockfordEncoding.encoding.decode(
    base32Encoded: "69RDT89E2T8BMB7SZVZ03F4SGV2"
) // 00c9c37484b85a42e8b3e7fbf806f2661b

let dataGroupedBy3 = try Base32CrockfordEncoding.encoding.decode(
    base32Encoded: "69R-DT8-9E2-T8B-MB7-SZV-Z03-F4S-GV2"
) // 00c9c37484b85a42e8b3e7fbf806f2661b

let dataGroupedBy9 = try Base32CrockfordEncoding.encoding.decode(
    base32Encoded: "69RDT89E2-T8BMB7SZV-Z03F4SGV2"
) // 00c9c37484b85a42e8b3e7fbf806f2661b

assert(dataNoHyphens == dataGroupedBy3) // true
assert(dataNoHyphens == dataGroupedBy9) // true
assert(dataGroupedBy3 == dataGroupedBy9) // true

Creating an Identifier

ThirtyTo offers the ability to create identifiers of different types in a universal factory. While UUID has been available to developers, this library provides a interface for creating two other types. To do this you can call IdentifierFactory/createIdentifier(with:) on the Identifier/factory to create any of the three types provided:

Each type has corresponding ComposableIdentifier/Specifications:

  • UUID takes no specifications and be created with IdentifierFactory.createIdentifier(_:)
  • ULID takes ULID/Specifications
  • UDID takes AnyIdentifierSpecifications

Here's an example with UDID:

// create an identifier for 1 million unique IDs that is a factor of 5.
let specs = AnyIdentifierSpecifications(
  size: .minimumCount(1_000_000, factorOf: 5)
)
let identifier: UDID = Identifier.factory.createIdentifier(with: specs)

What is ULID?

ULID serves the purpose of solving several issues with UUID while being compatible:

  • 1.21e+24 unique ULIDs per millisecond
  • Lexicographically sortable!
  • Monotonic sort order (correctly detects and handles the same millisecond)

Most importantly it Uses Base32Crockford for better efficiency and readability.

To create a ULID you can either use IdentifierFactory.createIdentifier(with:) on Identifier.factory:

let ulid : ULID = Identifier.factory.createIdentifier(with: .parts(nil, .random(nil)))

or a constructor:

let ulid = ULID(specifications: .parts(nil, .random(nil)))

For most cases the default ULID.Specifications.default specification is sufficient. The follows the canonical spec which uses the first 6 bytes for a the timestamp and the last 10 bytes are random. Otherwise you can specify all 16 bytes with ULID.Specifications.data(_:) or specify which Date to use for the timestamp and the RandomDataGenerator to use for the ULID.randomPart of the data.

References

License

This code is distributed under the MIT license. See the LICENSE file for more info.