This commit is contained in:
randogoth 2026-09-13 00:11:23 +03:00
commit 07ae2a3b20
109 changed files with 169 additions and 0 deletions

7
Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "td36"
version = "0.1.0"

8
Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "td36"
version = "0.1.0"
edition = "2021"
description = "Time-delta identifiers encoded as unpadded base36 strings"
license = "MIT OR Apache-2.0"
[dependencies]

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# td36
Time-delta identifiers: a timestamp is stored as the base36 encoding of its offset from a chosen epoch, so ids stay short, sortable and monotonically increasing.
```rust
use td36::{Td36Epoch, Td36Precision};
let epoch = Td36Epoch::new(1_700_000_000, Td36Precision::Seconds);
let id = epoch.encode(1_700_000_123).unwrap();
assert_eq!(epoch.decode_id(&id).unwrap(), 1_700_000_123);
```
`Td36Epoch::encode_epoch_marker` / `decode_epoch_marker` round-trip the epoch itself, so a stream of ids can carry the epoch it is relative to.
No dependencies. `cargo test` covers epoch round-tripping and the before-epoch guard.

117
src/lib.rs Normal file
View file

@ -0,0 +1,117 @@
//! TD36 Epoch helpers.
//!
//! This module implements a time-delta based identifier format where
//! monotonically increasing timestamps are encoded as unpadded base36 strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Td36Precision {
Milliseconds,
Seconds,
}
#[derive(Debug, Clone, Copy)]
pub struct Td36Epoch {
epoch: u64,
precision: Td36Precision,
}
impl Td36Epoch {
pub fn new(epoch: u64, precision: Td36Precision) -> Self {
Self { epoch, precision }
}
pub fn from_base36_epoch(encoded: &str, precision: Td36Precision) -> Result<Self, String> {
let epoch = Self::from_base36(encoded)?;
Ok(Self { epoch, precision })
}
pub fn encode(&self, timestamp: u64) -> Result<String, String> {
if timestamp < self.epoch {
return Err("timestamp is earlier than the TD36 Epoch".to_string());
}
let delta = timestamp - self.epoch;
Ok(Self::to_base36(delta))
}
pub fn decode_id(&self, id: &str) -> Result<u64, String> {
let delta = Self::from_base36(id)?;
Ok(self.epoch + delta)
}
pub fn precision(&self) -> Td36Precision {
self.precision
}
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn encode_epoch_marker(epoch: u64) -> String {
Self::to_base36(epoch)
}
pub fn decode_epoch_marker(encoded: &str) -> Result<u64, String> {
Self::from_base36(encoded)
}
pub fn to_base36(value: u64) -> String {
const ALPHABET: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if value == 0 {
return "0".to_string();
}
let mut buf = Vec::new();
let mut n = value;
while n > 0 {
buf.push(ALPHABET[(n % 36) as usize] as char);
n /= 36;
}
buf.iter().rev().collect()
}
pub fn from_base36(s: &str) -> Result<u64, String> {
let mut value: u64 = 0;
for c in s.chars() {
let digit = match c {
'0'..='9' => (c as u8 - b'0') as u64,
'A'..='Z' => (c as u8 - b'A' + 10) as u64,
'a'..='z' => (c as u8 - b'a' + 10) as u64,
_ => return Err(format!("Invalid base36 digit: {}", c)),
};
if digit >= 36 {
return Err(format!("Invalid base36 digit: {}", c));
}
value = value
.checked_mul(36)
.and_then(|v| v.checked_add(digit))
.ok_or_else(|| "Overflow while parsing base36".to_string())?;
}
Ok(value)
}
}
#[cfg(test)]
mod tests {
use super::{Td36Epoch, Td36Precision};
#[test]
fn encode_decode_round_trip_seconds() {
let base = 1_700_000_000;
let epoch = Td36Epoch::new(base, Td36Precision::Seconds);
let id = epoch.encode(base + 123).unwrap();
assert_eq!(epoch.decode_id(&id).unwrap(), base + 123);
}
#[test]
fn encode_epoch_marker_matches_decode() {
let base = 1_700_000_000;
let encoded = Td36Epoch::encode_epoch_marker(base);
let decoded = Td36Epoch::decode_epoch_marker(&encoded).unwrap();
assert_eq!(decoded, base);
}
#[test]
fn rejects_timestamps_before_epoch() {
let epoch = Td36Epoch::new(1_700_000_000, Td36Precision::Seconds);
assert!(epoch.encode(1_699_999_999).is_err());
}
}

1
target/.rustc_info.json Normal file
View file

@ -0,0 +1 @@
{"rustc_fingerprint":359451510981390643,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/mnt/data/Projects/code/plunq/.devbox/virtenv/rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: x86_64-unknown-linux-gnu\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}}

3
target/CACHEDIR.TAG Normal file
View file

@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

0
target/debug/.cargo-lock Normal file
View file

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
b609d12708ccc8a0

View file

@ -0,0 +1 @@
{"rustc":1100337564441796057,"features":"[]","declared_features":"[]","target":14041237549299544172,"profile":8731458305071235362,"path":10763286916239946207,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/td36-3a4a7689a5243a1b/dep-lib-td36","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
73e11e1efc2d4fef

View file

@ -0,0 +1 @@
{"rustc":1100337564441796057,"features":"[]","declared_features":"[]","target":14041237549299544172,"profile":1722584277633009122,"path":10763286916239946207,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/td36-b45c253676b5049d/dep-test-lib-td36","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,7 @@
/mnt/data/Projects/code/td36/target/debug/deps/td36-3a4a7689a5243a1b.d: src/lib.rs
/mnt/data/Projects/code/td36/target/debug/deps/libtd36-3a4a7689a5243a1b.rlib: src/lib.rs
/mnt/data/Projects/code/td36/target/debug/deps/libtd36-3a4a7689a5243a1b.rmeta: src/lib.rs
src/lib.rs:

Binary file not shown.

View file

@ -0,0 +1,5 @@
/mnt/data/Projects/code/td36/target/debug/deps/td36-b45c253676b5049d.d: src/lib.rs
/mnt/data/Projects/code/td36/target/debug/deps/td36-b45c253676b5049d: src/lib.rs
src/lib.rs:

Some files were not shown because too many files have changed in this diff Show more