timeflake 0.1.0

Rust port of Timeflake, a 128-bit, roughly-ordered, URL-safe UUID
Documentation
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
use std::{
    collections::HashSet,
    thread,
    time::{Duration, SystemTime, UNIX_EPOCH},
};
use uuid::Uuid;

use crate::max_random_biguint;
use crate::{max_timeflake_biguint, Timeflake, MAX_TIMESTAMP};

#[test]
fn test_random() {
    let mut rng = rand::rng();
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards")
        .as_secs() as u64;

    for _ in 0..1000 {
        let flake = Timeflake::new_random(&mut rng);
        let timestamp = flake.timestamp();
        let random = flake.random();

        assert!(timestamp >= now, "Timestamp should be >= current time");
        assert!(timestamp <= MAX_TIMESTAMP, "Timestamp out of range");

        assert!(random.to_u128().is_some(), "Random component should be convertible to u128");
        let rand_value = random.to_u128().unwrap();
        assert!(rand_value <= max_random_biguint().to_u128().unwrap(), "Random value out of range");

        assert!(
            flake.to_bigint() >= &BigUint::zero(),
            "Flake int representation should be non-negative"
        );
        assert!(
            flake.to_bigint() <= &max_timeflake_biguint(),
            "Flake int representation out of range"
        );
    }
}

#[test]
fn test_from_values_timestamp_only() {
    let now = 123u64;

    for _ in 0..1000 {
        let flake = Timeflake::from_components(now, &BigUint::zero()).unwrap();

        let timestamp = flake.timestamp();
        let random = flake.random();

        assert_eq!(timestamp, now, "Timestamp should match the provided value");
        assert!(random.is_zero(), "Random component should be zero");

        assert!(
            flake.to_bigint() >= &BigUint::zero(),
            "Flake int representation should be non-negative"
        );
        assert!(
            flake.to_bigint() <= &max_timeflake_biguint(),
            "Flake int representation out of range"
        );

        assert!(random.to_u128().is_some(), "Random component should be convertible to u128");
        let rand_value = random.to_u128().unwrap();
        assert!(rand_value <= max_random_biguint().to_u128().unwrap(), "Random value out of range");
    }
}

#[test]
fn test_from_values_timestamp_and_random() {
    let now = 123u64;
    let rand = 456u128;
    let random_biguint = BigUint::from(rand);

    for _ in 0..1000 {
        let flake = Timeflake::from_components(now, &random_biguint).unwrap();

        let timestamp = flake.timestamp();
        let random = flake.random();

        assert_eq!(timestamp, now, "Timestamp should match the provided value");

        assert_eq!(
            random.to_u128().unwrap(),
            rand,
            "Random component should match the provided value"
        );

        assert!(
            flake.to_bigint() >= &BigUint::zero(),
            "Flake int representation should be non-negative"
        );
        assert!(
            flake.to_bigint() <= &max_timeflake_biguint(),
            "Flake int representation out of range"
        );

        assert!(random.to_u128().is_some(), "Random component should be convertible to u128");
        let rand_value = random.to_u128().unwrap();
        assert!(rand_value <= max_random_biguint().to_u128().unwrap(), "Random value out of range");
    }
}

#[test]
fn test_parse_base62_and_conversions() {
    let base62_str = "02i1KoFfY3auBS745gImbZ";
    let flake = Timeflake::from_base62(base62_str).unwrap();

    assert_eq!(flake.timestamp(), 1579091935216, "Timestamp should be 1579091935216");

    let expected_random = BigUint::parse_bytes(b"724773312193627487660233", 10).unwrap();
    assert_eq!(flake.random(), expected_random, "Random component mismatch");

    let expected_int_value =
        BigUint::parse_bytes(b"1909005012028578488143182045514754249", 10).unwrap();
    assert_eq!(flake.to_bigint(), &expected_int_value, "Flake int representation mismatch");
    assert_eq!(flake.to_hex(), "016fa936bff0997a0a3c428548fee8c9", "Hex representation mismatch");
    assert_eq!(flake.to_base62(), base62_str, "Base62 representation mismatch");
    assert_eq!(
        flake.to_bytes(),
        b"\x01o\xa96\xbf\xf0\x99z\n<B\x85H\xfe\xe8\xc9",
        "Byte representation mismatch"
    );

    let expected_uuid = Uuid::parse_str("016fa936-bff0-997a-0a3c-428548fee8c9").unwrap();
    assert_eq!(flake.to_uuid(), expected_uuid, "UUID representation mismatch");
}

#[test]
fn test_parse_bytes_and_conversions() {
    let byte_data: [u8; 16] = [
        0x01, 0x6f, 0xa9, 0x36, 0xbf, 0xf0, 0x99, 0x7a, 0x0a, 0x3c, 0x42, 0x85, 0x48, 0xfe, 0xe8,
        0xc9,
    ];
    let flake = Timeflake::from_bytes(byte_data).unwrap();

    assert_eq!(flake.timestamp(), 1579091935216, "Timestamp should be 1579091935216");

    let expected_random = BigUint::parse_bytes(b"724773312193627487660233", 10).unwrap();
    assert_eq!(flake.random(), expected_random, "Random component mismatch");

    let expected_int_value =
        BigUint::parse_bytes(b"1909005012028578488143182045514754249", 10).unwrap();
    assert_eq!(flake.to_bigint(), &expected_int_value, "Flake int representation mismatch");
    assert_eq!(flake.to_hex(), "016fa936bff0997a0a3c428548fee8c9", "Hex representation mismatch");
    assert_eq!(flake.to_base62(), "02i1KoFfY3auBS745gImbZ", "Base62 representation mismatch");
    assert_eq!(flake.to_bytes(), &byte_data, "Byte representation mismatch");

    let expected_uuid = Uuid::parse_str("016fa936-bff0-997a-0a3c-428548fee8c9").unwrap();
    assert_eq!(flake.to_uuid(), expected_uuid, "UUID representation mismatch");
}

#[test]
fn test_parse_hex_and_conversions() {
    let hex_str = "016fa936bff0997a0a3c428548fee8c9";
    let byte_data = hex::decode(hex_str).unwrap();
    let flake = Timeflake::from_bytes(byte_data.clone().try_into().unwrap()).unwrap();

    assert_eq!(flake.timestamp(), 1579091935216, "Timestamp should be 1579091935216");

    let expected_random = BigUint::parse_bytes(b"724773312193627487660233", 10).unwrap();
    assert_eq!(flake.random(), expected_random, "Random component mismatch");

    let expected_int_value =
        BigUint::parse_bytes(b"1909005012028578488143182045514754249", 10).unwrap();
    assert_eq!(flake.to_bigint(), &expected_int_value, "Flake int representation mismatch");
    assert_eq!(flake.to_hex(), hex_str, "Hex representation mismatch");
    assert_eq!(flake.to_base62(), "02i1KoFfY3auBS745gImbZ", "Base62 representation mismatch");
    assert_eq!(flake.to_bytes().to_vec(), byte_data, "Byte representation mismatch");

    let expected_uuid = Uuid::parse_str("016fa936-bff0-997a-0a3c-428548fee8c9").unwrap();
    assert_eq!(flake.to_uuid(), expected_uuid, "UUID representation mismatch");
}

#[test]
fn test_parse_int_and_conversions() {
    let int_value = BigUint::parse_bytes(b"1909005012028578488143182045514754249", 10).unwrap();
    let flake = Timeflake::from_bigint(int_value.clone()).unwrap();

    assert_eq!(flake.timestamp(), 1579091935216, "Timestamp should be 1579091935216");

    let expected_random = BigUint::parse_bytes(b"724773312193627487660233", 10).unwrap();
    assert_eq!(flake.random(), expected_random, "Random component mismatch");
    assert_eq!(flake.to_bigint(), &int_value, "Flake int representation mismatch");
    assert_eq!(flake.to_hex(), "016fa936bff0997a0a3c428548fee8c9", "Hex representation mismatch");
    assert_eq!(flake.to_base62(), "02i1KoFfY3auBS745gImbZ", "Base62 representation mismatch");

    assert_eq!(
        flake.to_bytes(),
        &[
            0x01, 0x6f, 0xa9, 0x36, 0xbf, 0xf0, 0x99, 0x7a, 0x0a, 0x3c, 0x42, 0x85, 0x48, 0xfe,
            0xe8, 0xc9
        ],
        "Byte representation mismatch"
    );

    let expected_uuid = Uuid::parse_str("016fa936-bff0-997a-0a3c-428548fee8c9").unwrap();
    assert_eq!(flake.to_uuid(), expected_uuid, "UUID representation mismatch");
}

#[test]
fn test_timestamp_increment() {
    let flake1 = Timeflake::new_random(&mut rand::rng());
    thread::sleep(Duration::from_millis(400));

    let flake2 = Timeflake::new_random(&mut rand::rng());
    thread::sleep(Duration::from_millis(1100));

    let flake3 = Timeflake::new_random(&mut rand::rng());

    assert!(
        flake1.to_bigint() < flake2.to_bigint() && flake2.to_bigint() < flake3.to_bigint(),
        "Flake order should be increasing"
    );

    let ts1 = flake1.timestamp();
    let ts2 = flake2.timestamp();
    let ts3 = flake3.timestamp();
    assert!(ts1 < ts2 && ts2 < ts3, "Timestamps should be strictly increasing");

    let timestamps = vec![ts1, ts2, ts3];
    let unique_timestamps: std::collections::HashSet<_> = timestamps.iter().collect();
    assert_eq!(unique_timestamps.len(), 3, "Timestamps should all be unique");
}

#[test]
fn test_uniqueness() {
    let mut seen = HashSet::new();
    let mut rng = rand::rng();

    for i in 0..1_000_000 {
        let flake = Timeflake::new_random(&mut rng);
        let key = flake.to_base62();

        if seen.contains(&key) {
            panic!("Flake collision found after {} generations", i);
        }
        assert_eq!(key.len(), 22, "Base62 key length should be 22, but got {}", key.len());

        seen.insert(key);
    }
}