BASE-60 (B-60) — A Human-Centric, Error-Resilient, Trigonometric-Friendly, 6-bit Text Encoding

Version 1.1 — 2026-09-06 | Public Domain (CC-0)

1. Introduction

B-60 encodes binary data into a compact, human-readable form. The encoding uses 60 printable symbols. Four additional symbols are reserved for control functions. Each symbol represents a 6-bit value (0-63). The encoding is URL-safe, QR-code-friendly, and terminal-friendly.

2. Scope & Goals

GoalHow B-60 satisfies it
Human orientationThe alphabet uses only unambiguous, printable ASCII characters. The default mode is case-insensitive. An optional mode preserves case.
Error sensitivityFour reserved symbols provide checksum and padding. The alphabet excludes visually confused characters (0, O, I, l).
TrigonometryB-60 supports sexagesimal angle notation (°, ', ") as semantic extensions. These characters do not break the binary encoding.
DivisibilityThe radix 60 = 3·4·5 gives many exact fractions. B-60 maps naturally to time and angle subdivisions.
Text transmissibilityAll characters are URL-safe and QR-code-friendly. No control characters are used.
Defined 6-bit encodingEach B-60 digit is a 6-bit value (0-63). 60 values are assigned to printable symbols. The remaining 4 are reserved for padding, checksum, error-detect, and future-use.

3. Terminology

TermDefinition
B-60 digitOne symbol from the B-60 alphabet that represents a 6-bit value (0-59).
B-60 blockA group of 4 B-60 digits (24 bits) that encodes 3 bytes of binary data.
PADThe padding symbol (_) that fills the final block when the input length is not a multiple of 3 bytes.
CHKA single-digit mod-60 checksum appended to the encoded string. This is optional but recommended.
S-modeSexagesimal mode. A syntactic wrapper that allows angle or time values (<B60>#…).
Case-preserving modeA mode that keeps the case of letters (A-Z, a-z). In the default case-insensitive mode, receivers can treat upper-case and lower-case as equivalent.

4. Alphabet

The B-60 alphabet has 60 usable symbols and 4 reserved symbols.

4.1 Usable Symbols (Values 0-59)

0
0
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
9
9
A
10
B
11
C
12
D
13
E
14
F
15
G
16
H
17
I
18
J
19
K
20
L
21
M
22
N
23
P
24
Q
25
R
26
S
27
T
28
U
29
V
30
W
31
X
32
Y
33
Z
34
a
35
b
36
c
37
d
38
e
39
f
40
g
41
h
42
i
43
j
44
k
45
m
46
n
47
p
48
q
49
r
50
s
51
t
52
u
53
v
54
w
55
x
56
y
57
z
58
~
59

4.2 Reserved Symbols (Values 60-63)

-
60 VER
_
61 PAD
=
62 CHK
*
63 FUT

4.3 Alphabet Summary

0123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz~-_=*

(60 usable symbols, 4 reserved symbols.)

4.4 Ambiguity Avoidance

The alphabet excludes these characters: O, l, +, /, = (except as checksum), and :. This eliminates common visual confusions. It also avoids characters with special meaning in URLs, shells, or QR-code error-correction algorithms.

5. 6-Bit Mapping

6-bit valueSymbol6-bit binary
0-90-9000000-001001
10-34A-Z (minus O)001010-100010
35-58a-z (minus o, l)100011-111010
59~111011
60-111100
61_ (PAD)111101
62= (CHK)111110
63* (FUT)111111

The mapping is monotonic. Higher values map to later symbols. This simplifies lookup tables and keeps the encoding deterministic.

6. Encoding Procedure

B-60 Encoding: 3 Bytes → 4 Symbols Byte 0 8 bits Byte 1 8 bits Byte 2 8 bits 24 bits total v0 6 bits v1 6 bits v2 6 bits v3 6 bits S0 S1 S2 S3 v0 = (byte0 >> 2) & 0x3F v1 = ((byte0 << 4) | (byte1 >> 4)) & 0x3F v2 = ((byte1 << 2) | (byte2 >> 6)) & 0x3F v3 = byte2 & 0x3F Padding (incomplete groups) 1 byte → v2=v3=61 (PAD) 2 bytes → v3=61 (PAD)

Use this procedure to encode binary data:

  1. Input. Accept an arbitrary binary blob B[0 … n-1].
  2. Chunk. Split the input into 3-byte groups. The last group can contain 1 or 2 bytes.
  3. Convert. Split each 3-byte group (24 bits) into four 6-bit values (v0 … v3). Use the standard Base-64 bit-shifting pattern:
    v0 = (byte0 >> 2) & 0x3F
    v1 = ((byte0 << 4) | (byte1 >> 4)) & 0x3F
    v2 = ((byte1 << 2) | (byte2 >> 6)) & 0x3F
    v3 = byte2 & 0x3F
    For incomplete groups:
    - 1 byte: produce v0, v1. Set v2 = v3 = 61 (PAD).
    - 2 bytes: produce v0, v1, v2. Set v3 = 61 (PAD).
  4. Map. Map each vi (0-63) to its symbol. Use the table in §5.
  5. Checksum. Compute a mod-60 checksum over the numeric values of the encoded digits. Exclude any PAD symbols.
    checksum = ( Σ vi ) mod 60
    Append the checksum digit. Then append the checksum marker (=). For example, if checksum = 27, the digit is R (value 27). The final string ends with R=.
  6. Result. Concatenate all symbols. Include any PAD symbols and the optional checksum block.

6.1 Example

Binary input (hex): 0xFA 0x3C 0x01 0x00

Example: 0xFA 0x3C 0x01 0x00 Block 1: FA 3C 01 0xFA 0x3C 0x01 62 (=) 35 (a) 48 (p) 1 (1) Block 2: 00 (pad) 0x00 0 (0) 0 (0) 61 (_) 61 (_) Result: =ap100__
BytesBits (24)6-bit groupsValuesSymbols
FA 3C 0111111010 00111100 00000001111110 100011 110000 00000162, 35, 48, 1=, a, p, 1
00 (pad)00000000 (only 1 byte)000000 000000 PAD PAD0, 0, 61, 610, 0, _, _

Encoded (without checksum): =ap100__

Checksum calculation:

Σ vi = 62 + 35 + 48 + 1 + 0 + 0 = 146
146 mod 60 = 26 → symbol = R (value 26)

Final B-60 string: =ap100__R=

The trailing = marks the presence of a checksum. The preceding R is the checksum digit.

7. Decoding Procedure

  1. Validate. Make sure that the string contains only allowed symbols. The allowed symbols are 0-9, A-Z, a-z, -, _, =, *.
  2. Detect checksum. If the string ends with = preceded by a valid B-60 digit, treat the last two characters as a checksum block. Verify that:
    ( Σ vi ) mod 60 == checksum_digit
    Exclude the PAD symbols (61) from the sum. If the check fails, reject the payload.
  3. Strip padding. Remove any trailing PAD symbols (_).
  4. Map. Convert each symbol back to its 6-bit value. Use the reverse table in §5.
  5. Re-assemble. For each group of four values (v0-v3), reconstruct the original three bytes. Use the inverse of the bit-shifts in §6.
  6. Output. Return the binary blob. If any step fails, return an error.

8. Error Detection & Resilience

If higher reliability is necessary, a full-block Reed-Solomon (RS(255,239)) can be layered on top of B-60. The reserved symbols are already allocated for such extensions.

9. Sexagesimal (Trigonometric / Time) Mode — S-mode

S-Mode Angle Encoding 45° 30' 15" Angle notation 163,815 seconds 0x027157 =ap100__ =ap100__#45°30'15"

B-60 coexists with the traditional sexagesimal notation used for angles and time:

<B60>#<degrees>°<minutes>'<seconds>"  // angle
<B60>#<hours>:<minutes>:<seconds>     // time

The prefix <B60># signals S-mode. The characters °, ', ", and : are outside the B-60 alphabet. They are semantic delimiters and are not encoded. The numeric parts (degrees, minutes, seconds, etc.) are encoded using the normal B-60 process after the #. The resulting string can be parsed by any B-60 decoder that recognises the # marker.

9.1 Example — Encoding an angle

Angle: 45° 30' 15"

  1. Convert to pure seconds:
    total_seconds = 45·3600 + 30·60 + 15 = 163,815
  2. Represent as a 3-byte integer (big-endian): 0x02 0x71 0x57.
  3. Encode with B-60 → =ap100__ (as in §6.1).
  4. Assemble S-mode string:
    =ap100__#45°30'15"

A decoder sees #, extracts the B-60 payload (=ap100__), decodes it, and reconstructs the original angle.

10. Divisibility & Fractional Support

Because 60 = 3·4·5, any fraction whose denominator divides 60 is exactly representable. B-60 therefore encourages these conventions:

DenominatorCommon useExample (decimal → B-60)
2Half-seconds, half-minutes, ½ hour0.5 → encode integer 30 (seconds) → U
3One-third of a minute (20 s)1/3 → encode 20 → K
4Quarter-hour (15 min)0.25 → encode 900 → g
5One-fifth of a degree (0.02°)0.2 → encode 12 → C
6One-sixth of a second1/6 → encode 10 → A
8One-eighth of a minute (7.5 s)0.125 → encode 7.5 → encode as 7 + fraction marker
10Deci-seconds, deci-minutes0.1 → encode 6 → 6
12One-twelfth of an hour (5 min)1/12 → encode 300 → z
15One-fifteenth of a degree (0.0666…°)1/15 → encode 4 → 4
20One-twentieth of a minute (3 s)1/20 → encode 3 → 3
30One-thirtieth of a second1/30 → encode 2 → 2

10.1 Fraction Marker

If an application needs to encode a non-integer value whose denominator does not divide 60, the future-use symbol * can be employed as a fraction marker:

<integer_part>*<numerator>/<denominator>

The decoder can treat the whole expression as a rational number. If desired, it can round to the nearest representable B-60 value. This feature is optional and not part of the core spec. Implementations can ignore it.

11. Text Transmissibility

12. Reserved Symbols — Future Extensions

SymbolValueSuggested use
-60Version tag — for example, -01 for version 1 of a protocol.
_61Padding — already defined.
=62Checksum marker — already defined.
*63Extension marker — optional fractional notation, custom metadata, or FEC block identifiers.

Implementations must not assign any other meaning to these symbols unless they also adopt the corresponding extension specification.

13. Security Considerations

  1. Checksum is not a cryptographic hash. It only detects accidental errors. Do not rely on it for integrity protection. Use HMAC, digital signatures, or authenticated encryption in addition to B-60.
  2. Padding leakage. As with Base-64, the presence of PAD symbols reveals the exact length modulo 3 of the original data. In privacy-critical contexts, consider encrypt-then-encode or apply length-hiding padding before B-60.
  3. Reserved symbols. The future-use symbols (-, *) must be treated as untrusted unless the protocol explicitly defines them. A malicious sender can inject protocol-level commands. Receivers must validate the context before they act on them.

14. Reference Implementation Guidelines

15. Python Codec

Below is the reference Python implementation. Save as b60.py and run directly or import as a module.

#!/usr/bin/env python3
"""BASE-60 (B-60) encoder/decoder — reference implementation per spec v1.1."""

from __future__ import annotations
import sys

# ── Alphabet tables ──────────────────────────────────────────────────────────

# Usable symbols (values 0-59)
_ALPHABET = (
    "0123456789"                      # 0-9
    "ABCDEFGHIJKLMNPQRSTUVWXYZ"        # 10-34 (no O; I IS included)
    "abcdefghijkmnpqrstuvwxyz"        # 35-58 (no o, l; i IS included)
    "~"                               # 59
)

assert len(_ALPHABET) == 60

# Reserved symbols (values 60-63)
_VER = "-"          # value 60 (version tag)
_PAD = "_"          # value 61 (padding)
_CHK = "="          # value 62 (checksum marker)
_FUT = "*"          # value 63 (future use / extension)
_RESERVED = _VER + _PAD + _CHK + _FUT

# Build lookup tables
_VAL_TO_SYM = list(_ALPHABET) + list(_RESERVED)
_SYM_TO_VAL: dict[str, int] = {s: i for i, s in enumerate(_VAL_TO_SYM)}
ALLOWED = set(_ALPHABET + _RESERVED)


# ── Encoding ──────────────────────────────────────────────────────────────────

def _encode_block(b0: int, b1: int, b2: int) -> tuple[int, int, int, int]:
    """Encode 3 bytes into four 6-bit values."""
    v0 = (b0 >> 2) & 0x3F
    v1 = ((b0 << 4) | (b1 >> 4)) & 0x3F
    v2 = ((b1 << 2) | (b2 >> 6)) & 0x3F
    v3 = b2 & 0x3F
    return v0, v1, v2, v3


def encode(data: bytes, checksum: bool = True) -> str:
    """Encode binary data to a B-60 string."""
    if not data:
        return ""

    result: list[str] = []
    values: list[int] = []
    n = len(data)
    i = 0

    while i < n:
        b0 = data[i]
        b1 = data[i + 1] if i + 1 < n else 0
        b2 = data[i + 2] if i + 2 < n else 0

        v0, v1, v2, v3 = _encode_block(b0, b1, b2)

        result.append(_VAL_TO_SYM[v0])
        result.append(_VAL_TO_SYM[v1])
        values.append(v0)
        values.append(v1)

        remaining = n - i
        if remaining >= 3:
            result.append(_VAL_TO_SYM[v2])
            result.append(_VAL_TO_SYM[v3])
            values.append(v2)
            values.append(v3)
        elif remaining == 2:
            result.append(_VAL_TO_SYM[v2])
            result.append(_PAD)
            values.append(v2)
        else:
            result.append(_PAD)
            result.append(_PAD)

        i += 3

    if checksum:
        chk = sum(values) % 60
        result.append(_VAL_TO_SYM[chk])
        result.append(_CHK)

    return "".join(result)


# ── Decoding ──────────────────────────────────────────────────────────────────

class B60Error(Exception):
    pass

class InvalidCharacterError(B60Error):
    pass

class ChecksumMismatchError(B60Error):
    pass


def decode(encoded: str, validate_checksum: bool = True) -> bytes:
    """Decode a B-60 string back to binary data."""
    if not encoded:
        return b""

    for ch in encoded:
        if ch not in ALLOWED:
            raise InvalidCharacterError(f"Invalid character {ch!r}")

    has_checksum = False
    chk_digit = -1
    payload = encoded

    if len(encoded) >= 2 and encoded[-1] == _CHK:
        has_checksum = True
        chk_sym = encoded[-2]
        chk_digit = _SYM_TO_VAL[chk_sym]
        payload = encoded[:-2]

    vals = [_SYM_TO_VAL[ch] for ch in payload]

    if has_checksum:
        data_vals = [v for v in vals if v != 61]
        expected = sum(data_vals) % 60
        if expected != chk_digit:
            raise ChecksumMismatchError(
                f"Checksum mismatch: expected {expected}, got {chk_digit}"
            )

    return _decode_payload(payload)


def _decode_payload(payload: str) -> bytes:
    """Decode payload string to bytes, handling padding correctly."""
    n = len(payload)
    out = bytearray()

    i = 0
    while i < n:
        block_end = min(i + 4, n)
        block = payload[i:block_end]
        vals = [_SYM_TO_VAL[ch] for ch in block]
        while len(vals) < 4:
            vals.append(0)

        v0, v1, v2, v3 = vals
        b0 = (v0 << 2) | (v1 >> 4)
        b1 = ((v1 & 0x0F) << 4) | (v2 >> 2)
        b2 = ((v2 & 0x03) << 6) | v3

        out.append(b0)
        out.append(b1)
        out.append(b2)
        i += 4

    # Trim trailing PADs
    last_block_start = (n - 1) // 4 * 4
    last_block = payload[last_block_start:]
    trailing_pads = 0
    for ch in reversed(last_block):
        if ch == _PAD:
            trailing_pads += 1
        else:
            break

    if trailing_pads == 1:
        out = out[:-1]
    elif trailing_pads == 2:
        out = out[:-2]

    return bytes(out)


# ── S-Mode ────────────────────────────────────────────────────────────────────

def encode_angle(degrees: int, minutes: int, seconds: int,
                 checksum: bool = True) -> str:
    """Encode an angle as S-mode string."""
    total_seconds = degrees * 3600 + minutes * 60 + seconds
    data = total_seconds.to_bytes(3, "big")
    b60_part = encode(data, checksum=checksum)
    return f"{b60_part}#{degrees}°{minutes}'{seconds}\\""


def decode_angle(s_mode: str) -> tuple[int, int, int]:
    """Decode an S-mode angle string."""
    if "#" not in s_mode:
        raise B60Error("Not an S-mode string: missing '#' marker")

    b60_part, _ = s_mode.split("#", 1)
    raw = decode(b60_part)
    if len(raw) < 3:
        raw = b"\x00" * (3 - len(raw)) + raw

    total_seconds = int.from_bytes(raw[:3], "big")
    degrees = total_seconds // 3600
    minutes = (total_seconds % 3600) // 60
    seconds = total_seconds % 60
    return degrees, minutes, seconds


# ── CLI ───────────────────────────────────────────────────────────────────────

def main():
    import argparse
    parser = argparse.ArgumentParser(description="BASE-60 encoder/decoder")
    sub = parser.add_subparsers(dest="command")

    enc = sub.add_parser("encode", help="Encode binary/hex to B-60")
    enc.add_argument("data", help="Hex string or '-' for stdin")
    enc.add_argument("--no-checksum", action="store_true")

    dec = sub.add_parser("decode", help="Decode B-60 to hex")
    dec.add_argument("string", help="B-60 encoded string")
    dec.add_argument("--no-verify", action="store_true")

    args = parser.parse_args()

    if args.command == "encode":
        if args.data == "-":
            data = sys.stdin.buffer.read()
        else:
            data = bytes.fromhex(args.data)
        print(encode(data, checksum=not args.no_checksum))
    elif args.command == "decode":
        try:
            result = decode(args.string, validate_checksum=not args.no_verify)
            print(result.hex())
        except B60Error as e:
            print(f"Error: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
def encode(data: bytes, checksum: bool = True) -> str:
    """Encode binary data to a B-60 string."""
    if not data:
        return ""

    result = []
    values = []
    n = len(data)
    i = 0

    while i < n:
        b0 = data[i]
        b1 = data[i + 1] if i + 1 < n else 0
        b2 = data[i + 2] if i + 2 < n else 0

        v0 = (b0 >> 2) & 0x3F
        v1 = ((b0 << 4) | (b1 >> 4)) & 0x3F
        v2 = ((b1 << 2) | (b2 >> 6)) & 0x3F
        v3 = b2 & 0x3F

        result.append(_VAL_TO_SYM[v0])
        result.append(_VAL_TO_SYM[v1])
        values.extend([v0, v1])

        remaining = n - i
        if remaining >= 3:
            result.append(_VAL_TO_SYM[v2])
            result.append(_VAL_TO_SYM[v3])
            values.extend([v2, v3])
        elif remaining == 2:
            result.append(_VAL_TO_SYM[v2])
            result.append("_")
            values.append(v2)
        else:
            result.append("_")
            result.append("_")
        i += 3

    if checksum:
        chk = sum(values) % 60
        result.append(_VAL_TO_SYM[chk])
        result.append("=")

    return "".join(result)
def decode(encoded: str, validate_checksum: bool = True) -> bytes:
    """Decode a B-60 string back to binary data."""
    if not encoded:
        return b""

    for ch in encoded:
        if ch not in ALLOWED:
            raise InvalidCharacterError(f"Invalid character {ch!r}")

    has_checksum = False
    chk_digit = -1
    payload = encoded

    if len(encoded) >= 2 and encoded[-1] == "=":
        has_checksum = True
        chk_digit = _SYM_TO_VAL[encoded[-2]]
        payload = encoded[:-2]

    vals = [_SYM_TO_VAL[ch] for ch in payload]

    if has_checksum:
        data_vals = [v for v in vals if v != 61]
        expected = sum(data_vals) % 60
        if expected != chk_digit:
            raise ChecksumMismatchError()

    return _decode_payload(payload)
# Encode hex to B-60
$ python3 b60.py encode FA3C0100
=ap100__R=

# Decode B-60 to hex
$ python3 b60.py decode =ap100__R=
fa3c0100

# Encode without checksum
$ python3 b60.py encode --no-checksum FA3C0100
=ap100__

# Decode without verification
$ python3 b60.py decode --no-verify =ap100__
fa3c0100

# Pipe from stdin
$ echo -n "Hello" | python3 b60.py encode -
I6LjS6__j=

Interactive Encoder

Interactive Decoder

16. Bibliography & Prior Art

ReferenceRelevance
RFC 4648 — "The Base16, Base32, Base64 Data Encodings"Basis for block-wise 6-bit encoding.
Bitcoin Base-58 specificationShows the value of removing ambiguous characters.
Sumerian sexagesimal tablets (c. 2000 BC)Historical motivation for a base-60 system.
ISO 8601 — "Date and time format"Demonstrates modern use of sexagesimal divisions.
"Base-60: a compact human-readable numeric system" — chriskohl/base60 (GitHub)Existing experimental base-60 encoders.
QR-Code Specification (ISO/IEC 18004)Guidance on character set selection for scanning.

Concluding Remarks

Base-60 (B-60) merges the heritage of sexagesimal mathematics with the practicalities of modern digital communication:

This document is placed in the public domain (CC-0) and can be freely used, modified, and redistributed.