diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index ee78b58973..ce1258499b 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` + ### Fixed +* [JS/TS] Fix `Guid` to use cryptographically strong random values (by @ncave) +* [Python] Fix `DateTimeOffset` millisecond constructor and property (by @ncave) * [Python] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument emitting it as a start-index instead of a compile error (by @repo-assist) * [Beam] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument incorrectly treating the enum value as a start index diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 3de0f70abb..1fe7753e52 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` + ### Fixed +* [JS/TS] Fix `Guid` to use cryptographically strong random values (by @ncave) +* [Python] Fix `DateTimeOffset` millisecond constructor and property (by @ncave) * [Python] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument emitting it as a start-index instead of a compile error (by @repo-assist) * [Beam] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument incorrectly treating the enum value as a start index diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index b7a0d3a1fb..a44de13dbd 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -3877,6 +3877,7 @@ let private guids Helper.LibCall(com, "fable_guid", "from_bytes", t, [ arg ], ?loc = r) |> Some | _ -> None | "NewGuid" -> Helper.LibCall(com, "fable_guid", "new_guid", t, [], ?loc = r) |> Some + | "CreateVersion7" -> Helper.LibCall(com, "fable_guid", "create_version7", t, args, ?loc = r) |> Some | "Parse" -> match args with | [ StringConst literalGuid ] -> diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index ae56c31014..5f8dd6f1f0 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -3450,6 +3450,9 @@ let guids match i.CompiledName with | "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some + | "CreateVersion7" -> + Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes) + |> Some | "Parse" -> match args with | [ StringConst literalGuid ] -> parseGuid literalGuid diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 311d173e62..2c74811a07 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3494,6 +3494,9 @@ let guids = match i.CompiledName with | "NewGuid" -> Helper.LibCall(com, "guid", "new_guid", t, []) |> Some + | "CreateVersion7" -> + Helper.LibCall(com, "guid", "create_version7", t, args, i.SignatureArgTypes) + |> Some | "Parse" -> Helper.LibCall(com, "guid", "parse", t, args, i.SignatureArgTypes) |> Some | "TryParse" -> Helper.LibCall(com, "guid", "try_parse", t, args, i.SignatureArgTypes) |> Some | "ToByteArray" -> diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index d6a69464ed..0d82ed9084 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3822,6 +3822,9 @@ let guids match i.CompiledName with | "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some + | "CreateVersion7" -> + Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes) + |> Some | "Parse" -> match args with | [ StringConst literalGuid ] -> parseGuid literalGuid diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 861aeb0763..377d1e9c34 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -3121,6 +3121,10 @@ let guids | _ -> None // | "Empty", None, [] -> // it's a static field, see tryField | "NewGuid", None, [] -> Helper.LibCall(com, "Guid", "new_guid", t, args, ?loc = r) |> Some + | "CreateVersion7", None, [] -> Helper.LibCall(com, "Guid", "create_version7", t, [], ?loc = r) |> Some + | "CreateVersion7", None, _ -> + Helper.LibCall(com, "Guid", "create_version7_with_timestamp", t, args, ?loc = r) + |> Some | "Parse", None, [ ExprType String ] -> Helper.LibCall(com, "Guid", "parse", t, args, ?loc = r) |> Some | "TryParse", None, [ ExprType String; _ ] -> Helper.LibCall(com, "Guid", "tryParse", t, args, ?loc = r) |> Some | "ToByteArray", Some x, [] -> Helper.LibCall(com, "Guid", "toByteArray", t, [ x ], ?loc = r) |> Some diff --git a/src/fable-library-beam/fable_guid.erl b/src/fable-library-beam/fable_guid.erl index a9ba857b70..074435f75d 100644 --- a/src/fable-library-beam/fable_guid.erl +++ b/src/fable-library-beam/fable_guid.erl @@ -1,9 +1,12 @@ -module(fable_guid). -export([ - new_guid/0, parse/1, try_parse/1, try_parse/2, from_bytes/1, to_byte_array/1, to_string_format/2 + new_guid/0, create_version7/0, create_version7/1, + parse/1, try_parse/1, try_parse/2, from_bytes/1, to_byte_array/1, to_string_format/2 ]). -spec new_guid() -> binary(). +-spec create_version7() -> binary(). +-spec create_version7(integer()) -> binary(). -spec parse(binary()) -> binary(). -spec try_parse(binary()) -> {boolean(), binary()}. -spec try_parse(binary(), reference()) -> boolean(). @@ -26,6 +29,37 @@ new_guid() -> ) ). +%% Generate a new UUID v7 (RFC 9562) with current timestamp. +create_version7() -> + Ms = erlang:system_time(millisecond), + create_version7(Ms). + +%% Generate a new UUID v7 (RFC 9562) with given DateTimeOffset or ms since Unix epoch. +create_version7({Ticks, _Kind, _Offset}) -> + %% DateTimeOffset tuple: convert .NET ticks to Unix epoch milliseconds + %% .NET ticks are 100ns intervals since 0001-01-01 + %% Unix epoch in .NET ticks = 621355968000000000 + Ms = (Ticks - 621355968000000000) div 10000, + create_version7(Ms); +create_version7(Timestamp) when is_integer(Timestamp) -> + <> = crypto:strong_rand_bytes(10), + RandA = RandA0 band 16#0FFF, + RandB = RandB0 band 16#3FFFFFFFFFFFFFFF, + %% 48-bit timestamp split into A (high 32 bits) and B (low 16 bits) + A = (Timestamp bsr 16) band 16#FFFFFFFF, + B = Timestamp band 16#FFFF, + %% Version 7: top 4 bits = 0111 + C = 16#7000 bor RandA, + %% Variant: top 2 bits = 10 + D = 16#8000 bor ((RandB bsr 48) band 16#3FFF), + E = RandB band 16#FFFFFFFFFFFF, + list_to_binary( + io_lib:format( + "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b", + [A, B, C, D, E] + ) + ). + %% Construct a GUID from a 16-byte array (binary or list of integers). %% .NET uses mixed-endian: first 3 groups are little-endian, last 2 are big-endian. from_bytes({byte_array, _, _} = BA) -> diff --git a/src/fable-library-py/fable_library/date_offset.py b/src/fable-library-py/fable_library/date_offset.py index 22dbd0285c..a06c4219f2 100644 --- a/src/fable-library-py/fable_library/date_offset.py +++ b/src/fable-library-py/fable_library/date_offset.py @@ -149,7 +149,7 @@ def create( ms = 0 if python_offset is None: - dt = datetime(year, month, day, h, m, s, ms) + dt = datetime(year, month, day, h, m, s, ms * 1000) if offset is not None: offset_ms = int(time_span.total_microseconds(offset) / 1000) python_offset = timedelta(microseconds=float(time_span.total_microseconds(offset))) @@ -207,6 +207,14 @@ def second(d: DateTimeOffset) -> int: return d.second +def millisecond(d: DateTimeOffset) -> int: + return d.microsecond // 1000 + + +def microsecond(d: DateTimeOffset) -> int: + return d.microsecond % 1000 + + def op_subtraction(x: DateTimeOffset, y: DateTimeOffset | TimeSpan) -> DateTimeOffset | TimeSpan: if isinstance(y, TimeSpan): # Subtract TimeSpan from DateTimeOffset @@ -231,6 +239,8 @@ def op_subtraction(x: DateTimeOffset, y: DateTimeOffset | TimeSpan) -> DateTimeO "op_subtraction", "parse", "second", + "millisecond", + "microsecond", "try_parse", "utc_now", "year", diff --git a/src/fable-library-py/fable_library/guid.py b/src/fable-library-py/fable_library/guid.py index b1a291fe83..114d12c21d 100644 --- a/src/fable-library-py/fable_library/guid.py +++ b/src/fable-library-py/fable_library/guid.py @@ -1,7 +1,10 @@ +import secrets +import time import uuid from .array_ import Array from .core import FSharpRef, byte +from .date_offset import DateTimeOffset def parse(string: str) -> uuid.UUID: @@ -32,8 +35,38 @@ def array_to_guid(guid: Array[byte]) -> uuid.UUID: return uuid.UUID(bytes_le=bytes(guid)) +def create_version7(timestamp: DateTimeOffset | None = None) -> uuid.UUID: + """Create a UUID v7 per RFC 9562.""" + if timestamp is None: + ms = int(time.time() * 1000) + else: + # DateTimeOffset has getTime() returning ms since epoch + ms = int(timestamp.getTime()) + + # Build 16 bytes per RFC 9562 + rand_bytes = secrets.token_bytes(10) + + # Bytes 0-5: 48-bit big-endian ms timestamp + ts_bytes = ms.to_bytes(6, byteorder="big") + + # Bytes 6-7: version (0111) + 12 bits rand_a + rand_a = int.from_bytes(rand_bytes[0:2], "big") + ver_rand_a = (0x7000 | (rand_a & 0x0FFF)).to_bytes(2, "big") + + # Bytes 8-9: variant (10) + 14 bits rand_b + rand_b_hi = int.from_bytes(rand_bytes[2:4], "big") + var_rand_b = (0x8000 | (rand_b_hi & 0x3FFF)).to_bytes(2, "big") + + # Bytes 10-15: 48 bits rand_b continued + rand_rest = rand_bytes[4:10] + + all_bytes = ts_bytes + ver_rand_a + var_rand_b + rand_rest + return uuid.UUID(bytes=bytes(all_bytes)) + + __all__ = [ "array_to_guid", + "create_version7", "guid_to_array", "new_guid", "parse", diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index 093e229017..0244c55b69 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -31,7 +31,7 @@ rust_decimal = { version = "1.40", features = ["maths"], default-features = fals startup = { version = "0.1", path = "vendored/startup", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -uuid = { version = "1.22", features = ["v4"], default-features = false, optional = true } +uuid = { version = "1.23", features = ["v4", "v7"], default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -uuid = { version = "1.22", features = ["v4", "js"], default-features = false, optional = true } +uuid = { version = "1.23", features = ["v4", "v7", "js"], default-features = false, optional = true } diff --git a/src/fable-library-rust/src/DateTimeOffset.rs b/src/fable-library-rust/src/DateTimeOffset.rs index 419058d622..9f3ce85de5 100644 --- a/src/fable-library-rust/src/DateTimeOffset.rs +++ b/src/fable-library-rust/src/DateTimeOffset.rs @@ -277,6 +277,10 @@ pub mod DateTimeOffset_ { self.0.timestamp() } + pub fn toUnixTimeSubsecNanos(&self) -> u32 { + self.0.timestamp_subsec_nanos() + } + pub fn year(&self) -> i32 { self.0.year() } diff --git a/src/fable-library-rust/src/Guid.rs b/src/fable-library-rust/src/Guid.rs index b7da890424..4037b7d3c1 100644 --- a/src/fable-library-rust/src/Guid.rs +++ b/src/fable-library-rust/src/Guid.rs @@ -1,9 +1,10 @@ #[cfg(feature = "guid")] pub mod Guid_ { + use crate::DateTimeOffset_::DateTimeOffset; use crate::NativeArray_::{new_array, Array}; use crate::Native_::{compare, MutCell}; use crate::String_::{string, toString}; - use uuid::Uuid; + use uuid::{NoContext, Timestamp, Uuid}; #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] pub struct Guid(Uuid); @@ -52,4 +53,16 @@ pub mod Guid_ { pub fn toByteArray(x: Guid) -> Array { new_array(&x.0.to_bytes_le()) } + + pub fn create_version7() -> Guid { + let dto = DateTimeOffset::utcNow(); + create_version7_with_timestamp(dto) + } + + pub fn create_version7_with_timestamp(dto: DateTimeOffset) -> Guid { + let seconds = dto.toUnixTimeSeconds() as u64; + let subsec_nanos = dto.toUnixTimeSubsecNanos(); + let ts = Timestamp::from_unix(NoContext, seconds, subsec_nanos); + Guid(Uuid::new_v7(ts)) + } } diff --git a/src/fable-library-ts/Guid.ts b/src/fable-library-ts/Guid.ts index b9a9ff2cd7..e74d2011a0 100644 --- a/src/fable-library-ts/Guid.ts +++ b/src/fable-library-ts/Guid.ts @@ -63,15 +63,38 @@ export function tryParse(str: string, defValue: FSharpRef): boolean { } } -// From https://gist.github.com/LeverOne/1308368 export function newGuid() { - let b = ""; - for (let a = 0; a++ < 36;) { - b += a * 51 & 52 - ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) - : "-"; - } - return b; + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1 + const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +// RFC 9562 UUID v7 +export function createVersion7(timestamp?: Date): string { + const ms = timestamp != null ? timestamp.getTime() : Date.now(); + + // 48-bit timestamp as hex + const msHex = Math.floor(ms).toString(16).padStart(12, "0"); + + // random bits + const bytes = new Uint8Array(10); + crypto.getRandomValues(bytes); + const view = new DataView(bytes.buffer); + const randA = view.getUint16(0) & 0x0fff; // 12 bits + const randB1 = view.getUint16(2) & 0x3fff; // 14 bits + const randB2 = view.getUint32(4); // 32 bits + const randB3 = view.getUint16(8); // 16 bits + + const timeLow = msHex.slice(0, 8); + const timeMid = msHex.slice(8, 12); + const ver = (0x7000 | randA).toString(16).padStart(4, "0"); + const variantAndRandB = (0x8000 | randB1).toString(16).padStart(4, "0"); + const node = randB2.toString(16).padStart(8, "0") + randB3.toString(16).padStart(4, "0"); + + return `${timeLow}-${timeMid}-${ver}-${variantAndRandB}-${node}`; } // Maps for number <-> hex string conversion diff --git a/tests/Beam/DateTimeOffsetTests.fs b/tests/Beam/DateTimeOffsetTests.fs index a0fe1da132..ae5d2cdd5f 100644 --- a/tests/Beam/DateTimeOffsetTests.fs +++ b/tests/Beam/DateTimeOffsetTests.fs @@ -35,6 +35,8 @@ let ``test DateTimeOffset constructors work`` () = d.Hour |> equal 13 d.Minute |> equal 23 d.Second |> equal 30 + let d2 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) + d2.Second + d2.Millisecond |> equal 530 [] let ``test DateTimeOffset.Offset works`` () = diff --git a/tests/Beam/GuidTests.fs b/tests/Beam/GuidTests.fs index c65a67317f..bdcb9c09e2 100644 --- a/tests/Beam/GuidTests.fs +++ b/tests/Beam/GuidTests.fs @@ -21,6 +21,37 @@ let ``test Guid.NewGuid creates different guids`` () = let g2 = Guid.NewGuid() g = g2 |> equal false +[] +let ``test Guid.CreateVersion7 works`` () = + let g1 = Guid.CreateVersion7() + let g2 = Guid.CreateVersion7() + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``test Guid.CreateVersion7 with DateTimeOffset works`` () = + let dto = DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero) + let g1 = Guid.CreateVersion7(dto) + let g2 = Guid.CreateVersion7(dto) + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``test Guid.CreateVersion7 is monotonic`` () = + [1 .. 100] + |> List.map (fun ms -> DateTimeOffset(2024, 1, 1, 0, 0, 0, ms, TimeSpan.Zero)) + |> List.map (fun dto -> Guid.CreateVersion7(dto)) + |> List.pairwise + |> List.iter (fun (g1, g2) -> g1 < g2 |> equal true) + [] let ``test Guid equality works`` () = let s = "f46d24f0-183e-4fca-9f47-e382afdfde8b" diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index c97a5e73f4..986d434c15 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -1119,6 +1119,36 @@ let tests = let g3 = Guid.Parse s1 g1 = g3 |> equal true + testCase "Guid.CreateVersion7 works" <| fun () -> + let g1 = Guid.CreateVersion7() + let g2 = Guid.CreateVersion7() + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + // Check version nibble is '7' + equal '7' s1[14] + // Check variant nibble is 8, 9, a, or b + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + + testCase "Guid.CreateVersion7 with DateTimeOffset works" <| fun () -> + let dto = DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero) + let g1 = Guid.CreateVersion7(dto) + let g2 = Guid.CreateVersion7(dto) + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + + testCase "Guid.CreateVersion7 is monotonic" <| fun () -> + [1 .. 100] + |> List.map (fun ms -> DateTimeOffset(2024, 1, 1, 0, 0, 0, ms, TimeSpan.Zero)) + |> List.map (fun dto -> Guid.CreateVersion7(dto)) + |> List.pairwise + |> List.iter (fun (g1, g2) -> g1 < g2 |> equal true) + testCase "Guid.Empty works" <| fun () -> let g1 = Guid.Empty string g1 |> equal "00000000-0000-0000-0000-000000000000" diff --git a/tests/Python/TestDateTimeOffset.fs b/tests/Python/TestDateTimeOffset.fs index 8e39fe878c..59a59b3394 100644 --- a/tests/Python/TestDateTimeOffset.fs +++ b/tests/Python/TestDateTimeOffset.fs @@ -37,6 +37,8 @@ let ``test DateTimeOffset constructors work`` () = d.Hour |> equal 13 d.Minute |> equal 23 d.Second |> equal 30 + let d2 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) + d2.Second + d2.Millisecond |> equal 530 [] let ``test DateTimeOffset.Offset works`` () = diff --git a/tests/Python/TestGuid.fs b/tests/Python/TestGuid.fs index 7505d68d71..04cf7aad89 100644 --- a/tests/Python/TestGuid.fs +++ b/tests/Python/TestGuid.fs @@ -21,6 +21,37 @@ let ``test Guid.NewGuid creates different guids`` () = let g2 = Guid.NewGuid() g = g2 |> equal false +[] +let ``test Guid.CreateVersion7 works`` () = + let g1 = Guid.CreateVersion7() + let g2 = Guid.CreateVersion7() + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``test Guid.CreateVersion7 with DateTimeOffset works`` () = + let dto = DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero) + let g1 = Guid.CreateVersion7(dto) + let g2 = Guid.CreateVersion7(dto) + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``test Guid.CreateVersion7 is monotonic`` () = + [1 .. 100] + |> List.map (fun ms -> DateTimeOffset(2024, 1, 1, 0, 0, 0, ms, TimeSpan.Zero)) + |> List.map (fun dto -> Guid.CreateVersion7(dto)) + |> List.pairwise + |> List.iter (fun (g1, g2) -> g1 < g2 |> equal true) + [] let ``test Guid equality works`` () = let s = "f46d24f0-183e-4fca-9f47-e382afdfde8b" diff --git a/tests/Rust/tests/src/GuidTests.fs b/tests/Rust/tests/src/GuidTests.fs index 1cb2c84d00..976b9267bf 100644 --- a/tests/Rust/tests/src/GuidTests.fs +++ b/tests/Rust/tests/src/GuidTests.fs @@ -49,6 +49,37 @@ let ``Guid.NewGuid works`` () = let g3 = Guid.Parse s1 g1 = g3 |> equal true +[] +let ``Guid.CreateVersion7 works`` () = + let g1 = Guid.CreateVersion7() + let g2 = Guid.CreateVersion7() + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``Guid.CreateVersion7 with DateTimeOffset works`` () = + let dto = DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero) + let g1 = Guid.CreateVersion7(dto) + let g2 = Guid.CreateVersion7(dto) + g1 = g2 |> equal false + let s1 = string g1 + equal 36 s1.Length + equal '7' s1[14] + let variantChar = s1[19] + (variantChar = '8' || variantChar = '9' || variantChar = 'a' || variantChar = 'b') |> equal true + +[] +let ``Guid.CreateVersion7 with DateTimeOffset is monotonic`` () = + [1 .. 100] + |> List.map (fun ms -> DateTimeOffset(2024, 1, 1, 0, 0, 0, ms, TimeSpan.Zero)) + |> List.map (fun dto -> Guid.CreateVersion7(dto)) + |> List.pairwise + |> List.iter (fun (g1, g2) -> g1 < g2 |> equal true) + // id is prefixed for guid creation as we check at compile time (if able) to create a string const [] let ``Guid.Parse works`` () =