feat: datetime,uuid
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/.cache
|
||||||
|
/build
|
||||||
39
CMakeLists.txt
Normal file
39
CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.30)
|
||||||
|
|
||||||
|
project(cpputils LANGUAGES CXX)
|
||||||
|
|
||||||
|
option(BUILD_SHARED_LIBS "Build cpputils as a shared library" OFF)
|
||||||
|
option(BUILD_TESTS "Build cpputils tests" OFF)
|
||||||
|
|
||||||
|
if(BUILD_SHARED_LIBS)
|
||||||
|
add_library(cpputils SHARED)
|
||||||
|
else()
|
||||||
|
add_library(cpputils STATIC)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set_target_properties(cpputils PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
target_sources(cpputils
|
||||||
|
PRIVATE
|
||||||
|
src/uuid.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cpputils
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||||
|
|
||||||
|
target_compile_features(cpputils PUBLIC cxx_std_17)
|
||||||
|
|
||||||
|
if(BUILD_TESTS)
|
||||||
|
add_executable(cpputils_tests
|
||||||
|
tests/test_main.cpp
|
||||||
|
tests/test_uuid.cpp)
|
||||||
|
|
||||||
|
target_include_directories(cpputils_tests PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||||
|
|
||||||
|
target_link_libraries(cpputils_tests PRIVATE cpputils)
|
||||||
|
|
||||||
|
target_compile_features(cpputils_tests PRIVATE cxx_std_17)
|
||||||
|
endif()
|
||||||
65
include/cpputils/datetime.h
Normal file
65
include/cpputils/datetime.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace cpputils {
|
||||||
|
namespace datetime {
|
||||||
|
|
||||||
|
inline auto print_iso8601_utc(std::chrono::system_clock::time_point tp)
|
||||||
|
-> std::string {
|
||||||
|
time_t t = std::chrono::system_clock::to_time_t(tp);
|
||||||
|
std::tm utc_tm = *std::gmtime(&t);
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::put_time(&utc_tm, "%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
return oss.str();
|
||||||
|
};
|
||||||
|
|
||||||
|
inline auto print_iso8601_local(std::chrono::system_clock::time_point tp)
|
||||||
|
-> std::string {
|
||||||
|
std::time_t time = std::chrono::system_clock::to_time_t(tp);
|
||||||
|
std::tm local_tm = *std::localtime(&time);
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::put_time(&local_tm, "%Y-%m-%dT%H:%M:%S");
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int64_t now() {
|
||||||
|
return static_cast<int64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::chrono::system_clock::time_point to_time_point(
|
||||||
|
int64_t timestamp_ms) {
|
||||||
|
return std::chrono::system_clock::time_point(
|
||||||
|
std::chrono::milliseconds(timestamp_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int64_t to_timestamp(std::chrono::system_clock::time_point tp) {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
tp.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int64_t to_timestamp_seconds(std::chrono::system_clock::time_point tp) {
|
||||||
|
return std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Duration>
|
||||||
|
inline int64_t add_to_timestamp(int64_t timestamp, Duration duration) {
|
||||||
|
auto time_point = std::chrono::system_clock::time_point(
|
||||||
|
std::chrono::milliseconds(timestamp));
|
||||||
|
time_point += duration;
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
time_point.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace datetime
|
||||||
|
} // namespace cpputils
|
||||||
53
include/cpputils/uuid.h
Normal file
53
include/cpputils/uuid.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace cpputils {
|
||||||
|
namespace uuid {
|
||||||
|
|
||||||
|
extern std::string uuidToString(std::array<uint8_t, 16> const& uuid) noexcept;
|
||||||
|
|
||||||
|
class V4 {
|
||||||
|
private:
|
||||||
|
std::mt19937_64 rng;
|
||||||
|
|
||||||
|
inline uint64_t random64() { return rng(); }
|
||||||
|
|
||||||
|
public:
|
||||||
|
inline V4() : rng(std::random_device{}()) {}
|
||||||
|
|
||||||
|
std::array<uint8_t, 16> generate();
|
||||||
|
};
|
||||||
|
|
||||||
|
class V7 {
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<int64_t> last_timestamp{0};
|
||||||
|
std::atomic<uint8_t> sequence{0};
|
||||||
|
std::mt19937_64 rng;
|
||||||
|
|
||||||
|
inline uint64_t random64() { return rng(); }
|
||||||
|
|
||||||
|
uint8_t next_sequence(int64_t current_timestamp);
|
||||||
|
|
||||||
|
public:
|
||||||
|
inline V7() : rng(std::random_device{}()) {}
|
||||||
|
|
||||||
|
static inline uint64_t get_timestamp_from_uuid(
|
||||||
|
std::array<uint8_t, 16> const& uuid) noexcept {
|
||||||
|
uint64_t timestamp = 0;
|
||||||
|
for (size_t i = 0; i < 6; ++i) {
|
||||||
|
timestamp = (timestamp << 8) | uuid[i];
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 16> generate();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uuid
|
||||||
|
} // namespace cpputils
|
||||||
119
src/uuid.cpp
Normal file
119
src/uuid.cpp
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#include <cpputils/datetime.h>
|
||||||
|
#include <cpputils/uuid.h>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using cpputils::uuid::V4;
|
||||||
|
using cpputils::uuid::V7;
|
||||||
|
|
||||||
|
std::string cpputils::uuid::uuidToString(
|
||||||
|
std::array<uint8_t, 16> const& uuid) noexcept {
|
||||||
|
static constexpr char hex[] = "0123456789abcdef";
|
||||||
|
|
||||||
|
std::string out(36, '\0');
|
||||||
|
size_t p = 0;
|
||||||
|
|
||||||
|
auto w = [&](size_t i) {
|
||||||
|
uint8_t b = uuid[i];
|
||||||
|
out[p++] = hex[b >> 4];
|
||||||
|
out[p++] = hex[b & 0xF];
|
||||||
|
};
|
||||||
|
|
||||||
|
w(0);
|
||||||
|
w(1);
|
||||||
|
w(2);
|
||||||
|
w(3);
|
||||||
|
out[p++] = '-';
|
||||||
|
w(4);
|
||||||
|
w(5);
|
||||||
|
out[p++] = '-';
|
||||||
|
w(6);
|
||||||
|
w(7);
|
||||||
|
out[p++] = '-';
|
||||||
|
w(8);
|
||||||
|
w(9);
|
||||||
|
out[p++] = '-';
|
||||||
|
w(10);
|
||||||
|
w(11);
|
||||||
|
w(12);
|
||||||
|
w(13);
|
||||||
|
w(14);
|
||||||
|
w(15);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 16> V4::generate() {
|
||||||
|
std::array<uint8_t, 16> uuid{};
|
||||||
|
uint64_t rand_a = random64();
|
||||||
|
uint64_t rand_b = random64();
|
||||||
|
uint64_t rand_c = random64();
|
||||||
|
// First 48 bits
|
||||||
|
uuid[0] = (rand_a >> 40) & 0xFF; // 0-7
|
||||||
|
uuid[1] = (rand_a >> 32) & 0xFF; // 8-15
|
||||||
|
uuid[2] = (rand_a >> 24) & 0xFF; // 16-23
|
||||||
|
uuid[3] = (rand_a >> 16) & 0xFF; // 24-31
|
||||||
|
uuid[4] = (rand_a >> 8) & 0xFF; // 32-39
|
||||||
|
uuid[5] = rand_a & 0xFF; // 40-47
|
||||||
|
|
||||||
|
uuid[6] = (rand_b) & 0x0F; // fill lower 4 bits with random
|
||||||
|
uuid[6] |= 0x40; // set top 4 bits to 0100
|
||||||
|
uuid[7] = (rand_b >> 8) & 0xFF;
|
||||||
|
|
||||||
|
uuid[8] = (rand_b >> 16) & 0xFF;
|
||||||
|
uuid[8] |= (0b10 << 6);
|
||||||
|
|
||||||
|
uuid[9] = (rand_c) & 0xFF;
|
||||||
|
uuid[10] = (rand_c >> 8) & 0xFF;
|
||||||
|
uuid[11] = (rand_c >> 16) & 0xFF;
|
||||||
|
uuid[12] = (rand_c >> 24) & 0xFF;
|
||||||
|
uuid[13] = (rand_c >> 32) & 0xFF;
|
||||||
|
uuid[14] = (rand_c >> 40) & 0xFF;
|
||||||
|
uuid[15] = (rand_c >> 48) & 0xFF;
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t V7::next_sequence(int64_t current_timestamp) {
|
||||||
|
uint64_t prev_timestamp = last_timestamp.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (current_timestamp > prev_timestamp) {
|
||||||
|
last_timestamp.store(current_timestamp, std::memory_order_relaxed);
|
||||||
|
sequence.store(0, std::memory_order_relaxed);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return sequence.fetch_add(1, std::memory_order_relaxed) & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 16> V7::generate() {
|
||||||
|
std::array<uint8_t, 16> uuid{};
|
||||||
|
int64_t timestamp = cpputils::datetime::now();
|
||||||
|
uint8_t seq = next_sequence(timestamp);
|
||||||
|
uint64_t rand_a = random64();
|
||||||
|
uint64_t rand_b = random64();
|
||||||
|
|
||||||
|
// Encode timestamp (big-endian)
|
||||||
|
uuid[0] = (timestamp >> 40) & 0xFF;
|
||||||
|
uuid[1] = (timestamp >> 32) & 0xFF;
|
||||||
|
uuid[2] = (timestamp >> 24) & 0xFF;
|
||||||
|
uuid[3] = (timestamp >> 16) & 0xFF;
|
||||||
|
uuid[4] = (timestamp >> 8) & 0xFF;
|
||||||
|
uuid[5] = timestamp & 0xFF;
|
||||||
|
|
||||||
|
// Version 7 (UUIDv7)
|
||||||
|
uuid[6] = ((timestamp >> 4) & 0x0F) | 0x70;
|
||||||
|
uuid[7] = ((timestamp << 4) & 0xF0) | ((seq >> 8) & 0x0F);
|
||||||
|
|
||||||
|
// Variant 1 (RFC 4122 standard)
|
||||||
|
uuid[8] = (seq & 0xFF) | 0x80;
|
||||||
|
|
||||||
|
// Fill remaining bytes with randomness
|
||||||
|
for (size_t i = 9; i < 16; i++) {
|
||||||
|
uuid[i] = (rand_b >> ((15 - i) * 8)) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
17975
tests/catch.hpp
Normal file
17975
tests/catch.hpp
Normal file
File diff suppressed because it is too large
Load Diff
2
tests/test_main.cpp
Normal file
2
tests/test_main.cpp
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define CATCH_CONFIG_MAIN
|
||||||
|
#include "catch.hpp"
|
||||||
43
tests/test_uuid.cpp
Normal file
43
tests/test_uuid.cpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <cpputils/uuid.h>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
TEST_CASE("uuidToString formats UUIDs correctly") {
|
||||||
|
|
||||||
|
struct TestVector {
|
||||||
|
std::array<uint8_t, 16> uuid;
|
||||||
|
char const* expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr TestVector tests[] = {
|
||||||
|
|
||||||
|
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00},
|
||||||
|
"00000000-0000-0000-0000-000000000000"},
|
||||||
|
|
||||||
|
{{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
|
||||||
|
0x89, 0xab, 0xcd, 0xef},
|
||||||
|
"01234567-89ab-cdef-0123-456789abcdef"},
|
||||||
|
|
||||||
|
{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff},
|
||||||
|
"ffffffff-ffff-ffff-ffff-ffffffffffff"},
|
||||||
|
|
||||||
|
{{0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66,
|
||||||
|
0x55, 0x44, 0x00, 0x00},
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000"},
|
||||||
|
|
||||||
|
{{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x80, 0x00, 0x01, 0x23,
|
||||||
|
0x45, 0x67, 0x89, 0xab},
|
||||||
|
"deadbeef-cafe-babe-8000-0123456789ab"},
|
||||||
|
|
||||||
|
{{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||||
|
0x0c, 0x0d, 0x0e, 0x0f},
|
||||||
|
"00010203-0405-0607-0809-0a0b0c0d0e0f"},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto const& t : tests) {
|
||||||
|
REQUIRE(cpputils::uuid::uuidToString(t.uuid) == t.expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user