Version 1.1 — 2026-09-06 | Public Domain (CC-0)
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.
| Goal | How B-60 satisfies it |
|---|---|
| Human orientation | The alphabet uses only unambiguous, printable ASCII characters. The default mode is case-insensitive. An optional mode preserves case. |
| Error sensitivity | Four reserved symbols provide checksum and padding. The alphabet excludes visually confused characters (0, O, I, l). |
| Trigonometry | B-60 supports sexagesimal angle notation (°, ', ") as semantic extensions. These characters do not break the binary encoding. |
| Divisibility | The radix 60 = 3·4·5 gives many exact fractions. B-60 maps naturally to time and angle subdivisions. |
| Text transmissibility | All characters are URL-safe and QR-code-friendly. No control characters are used. |
| Defined 6-bit encoding | Each 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. |
| Term | Definition |
|---|---|
| B-60 digit | One symbol from the B-60 alphabet that represents a 6-bit value (0-59). |
| B-60 block | A group of 4 B-60 digits (24 bits) that encodes 3 bytes of binary data. |
| PAD | The padding symbol (_) that fills the final block when the input length is not a multiple of 3 bytes. |
| CHK | A single-digit mod-60 checksum appended to the encoded string. This is optional but recommended. |
| S-mode | Sexagesimal mode. A syntactic wrapper that allows angle or time values (<B60>#…). |
| Case-preserving mode | A 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. |
The B-60 alphabet has 60 usable symbols and 4 reserved symbols.
0123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz~-_=*
(60 usable symbols, 4 reserved symbols.)
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.
| 6-bit value | Symbol | 6-bit binary |
|---|---|---|
| 0-9 | 0-9 | 000000-001001 |
| 10-34 | A-Z (minus O) | 001010-100010 |
| 35-58 | a-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.
Use this procedure to encode binary data:
v0 = (byte0 >> 2) & 0x3F
v1 = ((byte0 << 4) | (byte1 >> 4)) & 0x3F
v2 = ((byte1 << 2) | (byte2 >> 6)) & 0x3F
v3 = byte2 & 0x3F
For incomplete groups: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=.Binary input (hex): 0xFA 0x3C 0x01 0x00
| Bytes | Bits (24) | 6-bit groups | Values | Symbols |
|---|---|---|---|---|
| FA 3C 01 | 11111010 00111100 00000001 | 111110 100011 110000 000001 | 62, 35, 48, 1 | =, a, p, 1 |
| 00 (pad) | 00000000 (only 1 byte) | 000000 000000 PAD PAD | 0, 0, 61, 61 | 0, 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.
( Σ vi ) mod 60 == checksum_digit
Exclude the PAD symbols (61) from the sum. If the check fails, reject the payload.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.
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.
Angle: 45° 30' 15"
total_seconds = 45·3600 + 30·60 + 15 = 163,8150x02 0x71 0x57.=ap100__ (as in §6.1).=ap100__#45°30'15"A decoder sees #, extracts the B-60 payload (=ap100__), decodes it, and reconstructs the original angle.
Because 60 = 3·4·5, any fraction whose denominator divides 60 is exactly representable. B-60 therefore encourages these conventions:
| Denominator | Common use | Example (decimal → B-60) |
|---|---|---|
| 2 | Half-seconds, half-minutes, ½ hour | 0.5 → encode integer 30 (seconds) → U |
| 3 | One-third of a minute (20 s) | 1/3 → encode 20 → K |
| 4 | Quarter-hour (15 min) | 0.25 → encode 900 → g |
| 5 | One-fifth of a degree (0.02°) | 0.2 → encode 12 → C |
| 6 | One-sixth of a second | 1/6 → encode 10 → A |
| 8 | One-eighth of a minute (7.5 s) | 0.125 → encode 7.5 → encode as 7 + fraction marker |
| 10 | Deci-seconds, deci-minutes | 0.1 → encode 6 → 6 |
| 12 | One-twelfth of an hour (5 min) | 1/12 → encode 300 → z |
| 15 | One-fifteenth of a degree (0.0666…°) | 1/15 → encode 4 → 4 |
| 20 | One-twentieth of a minute (3 s) | 1/20 → encode 3 → 3 |
| 30 | One-thirtieth of a second | 1/30 → encode 2 → 2 |
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.
| Symbol | Value | Suggested use |
|---|---|---|
| - | 60 | Version tag — for example, -01 for version 1 of a protocol. |
| _ | 61 | Padding — already defined. |
| = | 62 | Checksum marker — already defined. |
| * | 63 | Extension 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.
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=
| Reference | Relevance |
|---|---|
| RFC 4648 — "The Base16, Base32, Base64 Data Encodings" | Basis for block-wise 6-bit encoding. |
| Bitcoin Base-58 specification | Shows 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. |
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.