Skip to main content

nautilus_binance/common/
encoder.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Deterministic two-way encoder for Binance Link broker ID prefixing.
17//!
18//! The Binance broker ID is automatically prefixed to supported system-generated
19//! client order IDs placed through the Binance adapter. Prefixing is transparent
20//! to strategies and requires no user configuration.
21//! Inbound order events are decoded back to the original `ClientOrderId`
22//! before reaching the trading system.
23//!
24//! Binance's [Link and Trade] program requires the `newClientOrderId`
25//! parameter to start with `x-{BROKER_ID}` for order attribution. Binance
26//! enforces a 36-character limit on this field with the regex
27//! `^[\.A-Z\:/a-z0-9_-]{1,36}$`.
28//!
29//! Internal Nautilus `ClientOrderId` values (O-format: 23+ chars, UUID: 32-36
30//! chars) exceed the 36-char limit when combined with the broker prefix. This
31//! module provides compact, deterministic, two-way encoding via pure functions.
32//!
33//! [Link and Trade]: https://www.binance.com/en/support/faq/detail/a78a065d0c4846aaa1af474d8e712ab9
34//!
35//! # Wire format
36//!
37//! ```text
38//! Legacy:    x-TD67BGP9-{signal}{base62_payload}
39//! Short tag: x-TD67BGP9A{base62_payload}
40//! ```
41//!
42//! The prefix `x-{BROKER_ID}-` is 11 chars (for an 8-char broker ID), leaving
43//! 25 chars for the encoded component. Spot and Futures use separate broker
44//! IDs defined in [`consts`](super::consts). The supplementary `A` format omits
45//! the separator after the broker ID and occupies 24 characters in total.
46//!
47//! # Signal chars
48//!
49//! The first character after the prefix identifies the original format so the
50//! decoder can reconstruct the exact original `ClientOrderId` string.
51//!
52//! | Signal | Original format            | Payload length | Total |
53//! |--------|----------------------------|----------------|-------|
54//! | `T`    | O-format with hyphens      | 13 base62      | 25    |
55//! | `t`    | O-format without hyphens   | 13 base62      | 25    |
56//! | `U`    | UUID with hyphens          | 22 base62      | 34    |
57//! | `u`    | UUID without hyphens       | 22 base62      | 34    |
58//! | `R`    | Raw passthrough            | variable       | <= 36 |
59//! | `A`    | O-format with a short tag  | 13 base62      | 24    |
60//!
61//! # O-format packing (72 bits -> 13 base62 chars)
62//!
63//! The O-format `ClientOrderId` `O-YYYYMMDD-HHMMSS-TTT-SSS-CCC` is packed
64//! into a 72-bit integer:
65//!
66//! ```text
67//! bits [71:40] (32 bits): seconds since 2020-01-01 epoch
68//! bits [39:30] (10 bits): trader tag (0-1023)
69//! bits [29:20] (10 bits): strategy tag (0-1023)
70//! bits [19:0]  (20 bits): count (0-1048575)
71//! ```
72//!
73//! # Short-tag O-format packing (77 bits -> 13 base62 chars)
74//!
75//! The `A` format preserves hyphenated factory IDs such as
76//! `O-20260922-160119-V2-000-8`. One tag contains one or two ASCII alphanumeric
77//! characters; the other is a canonical numeric tag in `[0, 1023]`, padded to
78//! at least three digits. The count is in `[0, 4194303]` without leading zeros.
79//! Timestamps cover `[2020-01-01 00:00:00, 2156-02-07 06:28:15]` UTC.
80//!
81//! ```text
82//! bits [76:45] (32 bits): seconds since 2020-01-01 epoch
83//! bit  [44]    (1 bit):  short tag is the strategy tag
84//! bits [43:32] (12 bits): one/two-character base62 tag, with length
85//! bits [31:22] (10 bits): numeric tag
86//! bits [21:0]  (22 bits): count
87//! ```
88//!
89//! Exact reconstruction preserves tag spelling and rejects normalized dates.
90//! Existing numeric, UUID, and raw encodings take precedence and retain their
91//! wire representation. Historical unprefixed fallback outputs contain at least
92//! 25 characters, so the 24-character supplementary format cannot be mistaken
93//! for any historical encoder output. A custom ID resembling this format is
94//! short enough to use the existing `R` wrapper.
95//!
96//! IDs outside the supported compact formats and raw prefix budget are sent
97//! unchanged with a warning; Binance's length and character limits still apply.
98//!
99//! # UUID packing (128 bits -> 22 base62 chars)
100//!
101//! The UUID is parsed from hex into a 128-bit integer and base62-encoded.
102//!
103//! # Decoding
104//!
105//! The decoder recognizes the 24-character `x-{BROKER_ID}A` format first,
106//! then the legacy `x-{BROKER_ID}-` signal formats. Other strings pass through
107//! unchanged, including historical unprefixed IDs. This keeps a longer custom
108//! ID starting with `x-{BROKER_ID}A` distinct from the short-tag format.
109//!
110//! Existing-order operations without a venue order ID query both supplementary
111//! and historical unprefixed identities. Different matching venue IDs are
112//! rejected as ambiguous before modification or cancellation.
113//!
114//! # Performance
115//!
116//! Base62 encoding uses a stack buffer and ten-digit chunks to limit wide
117//! integer division. Prefix construction writes directly into the result string.
118//! The `bench_encode_decode_timing` test provides a local timing check.
119//! Existing-order compatibility resolution may add two HTTP lookups.
120//!
121//! An isolated codec comparison compiled with `opt-level=3` measures the following
122//! median nanoseconds per operation over 25 interleaved batches of 100,000 calls.
123//! Timings are machine-dependent and exclude HTTP requests.
124//!
125//! | Format                   | Encode before | Encode after | Decode before | Decode after |
126//! | ------------------------ | ------------- | ------------ | ------------- | ------------ |
127//! | O-format with hyphens    | 102           | 44           | 201           | 124          |
128//! | O-format without hyphens | 99            | 41           | 192           | 116          |
129//! | UUID with hyphens        | 243           | 60           | 72            | 47           |
130//! | UUID without hyphens     | 240           | 57           | 67            | 43           |
131//! | Raw prefixed ID          | 34            | 16           | 39            | 21           |
132//! | Short-tag O-format       | N/A           | 174          | N/A           | 117          |
133//!
134//! Short-tag IDs previously used unprefixed passthrough (37 ns encode, 38 ns decode),
135//! which performs no compression and is not equivalent to short-tag encoding.
136
137use std::fmt::Write;
138
139use anyhow::Context;
140use nautilus_model::identifiers::ClientOrderId;
141
142/// Base62 encoding alphabet: `0-9 A-Z a-z`.
143const BASE62_CHARS: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
144
145/// Lookup table mapping ASCII byte values to base62 digit values.
146/// Invalid characters map to `0xFF`.
147const BASE62_DECODE: [u8; 128] = {
148    let mut table = [0xFFu8; 128];
149    let mut i = 0u8;
150    while i < 62 {
151        table[BASE62_CHARS[i as usize] as usize] = i;
152        i += 1;
153    }
154    table
155};
156
157/// Base epoch for O-format timestamp encoding: 2020-01-01 00:00:00 UTC.
158const O_FORMAT_EPOCH: i64 = 1_577_836_800;
159
160/// Fixed base62 output length for O-format packed values (72 bits).
161const O_FORMAT_B62_LEN: usize = 13;
162
163/// Fixed base62 output length for UUID packed values (128 bits).
164const UUID_B62_LEN: usize = 22;
165
166/// Maximum `newClientOrderId` length allowed by the Binance API.
167const MAX_CLIENT_ORDER_ID_LEN: usize = 36;
168
169const SIGNAL_O_HYPHENS: u8 = b'T';
170const SIGNAL_O_NO_HYPHENS: u8 = b't';
171const SIGNAL_UUID_HYPHENS: u8 = b'U';
172const SIGNAL_UUID_NO_HYPHENS: u8 = b'u';
173const SIGNAL_RAW: u8 = b'R';
174const SIGNAL_O_TAGS: u8 = b'A';
175const O_TAGS_B62_LEN: usize = 13;
176const O_TAGS_COUNT_BITS: u32 = 22;
177const O_TAGS_COUNT_MAX: u32 = (1 << O_TAGS_COUNT_BITS) - 1;
178
179/// Encodes a `ClientOrderId` into a Binance-compatible string with broker ID
180/// prefix.
181///
182/// The encoding is deterministic and reversible with [`decode_broker_id`].
183#[must_use]
184pub fn encode_broker_id(client_order_id: &ClientOrderId, broker_id: &str) -> String {
185    let id_str = client_order_id.as_str();
186    let budget = MAX_CLIENT_ORDER_ID_LEN - (3 + broker_id.len());
187
188    if let Some((packed, has_hyphens)) = pack_o_format(id_str) {
189        let signal = if has_hyphens {
190            SIGNAL_O_HYPHENS
191        } else {
192            SIGNAL_O_NO_HYPHENS
193        };
194        let b62 = encode_base62::<O_FORMAT_B62_LEN>(packed);
195        return build_encoded(broker_id, signal, &b62);
196    }
197
198    if let Some((value, has_hyphens)) = parse_uuid_hex(id_str) {
199        let signal = if has_hyphens {
200            SIGNAL_UUID_HYPHENS
201        } else {
202            SIGNAL_UUID_NO_HYPHENS
203        };
204        let b62 = encode_base62::<UUID_B62_LEN>(value);
205        return build_encoded(broker_id, signal, &b62);
206    }
207
208    if id_str.len() < budget {
209        return build_encoded(broker_id, SIGNAL_RAW, id_str.as_bytes());
210    }
211
212    // Keep the new format shorter than any historical passthrough ID
213    if 3 + broker_id.len() + O_TAGS_B62_LEN < budget
214        && let Some(packed) = pack_o_tags(id_str)
215    {
216        return build_encoded(
217            broker_id,
218            SIGNAL_O_TAGS,
219            &encode_base62::<O_TAGS_B62_LEN>(packed),
220        );
221    }
222
223    log::warn!(
224        "ClientOrderId '{id_str}' ({} chars) exceeds broker ID encoding budget ({budget} chars), sending without prefix",
225        id_str.len(),
226    );
227    id_str.to_string()
228}
229
230/// Decodes an encoded string back to the original `ClientOrderId` value.
231///
232/// If the string starts with a known broker prefix, the payload is decoded and
233/// the original ID is reconstructed. Strings without a recognized prefix are
234/// returned as-is for backward compatibility.
235#[must_use]
236pub fn decode_broker_id(encoded: &str, broker_id: &str) -> String {
237    match decode_broker_id_checked(encoded, broker_id) {
238        Ok(decoded) => decoded,
239        Err(e) => {
240            log::warn!("Failed to decode broker client order ID: {e}");
241            encoded.to_string()
242        }
243    }
244}
245
246/// Decodes and validates an inbound Binance client order ID.
247///
248/// Strings without the expected broker prefix are treated as legacy IDs and
249/// validated without decoding.
250///
251/// # Errors
252///
253/// Returns an error if the broker-prefixed encoding is malformed or the
254/// decoded client order ID is invalid.
255pub(crate) fn decode_client_order_id(
256    encoded: &str,
257    broker_id: &str,
258) -> anyhow::Result<ClientOrderId> {
259    let decoded = decode_broker_id_checked(encoded, broker_id)?;
260    ClientOrderId::new_checked(decoded)
261        .with_context(|| format!("invalid Binance client order ID '{encoded}'"))
262}
263
264fn decode_broker_id_checked(encoded: &str, broker_id: &str) -> anyhow::Result<String> {
265    if let Some(data) = tagged_payload(encoded, broker_id) {
266        let packed =
267            decode_base62(data).context("invalid tagged O-format broker client order ID")?;
268        return unpack_o_tags(packed);
269    }
270
271    let Some(payload) = encoded
272        .strip_prefix("x-")
273        .and_then(|s| s.strip_prefix(broker_id))
274        .and_then(|s| s.strip_prefix('-'))
275    else {
276        return Ok(encoded.to_string());
277    };
278
279    let Some((&signal, data)) = payload.as_bytes().split_first() else {
280        anyhow::bail!("missing broker client order ID signal");
281    };
282
283    match signal {
284        SIGNAL_O_HYPHENS | SIGNAL_O_NO_HYPHENS => {
285            anyhow::ensure!(
286                data.len() == O_FORMAT_B62_LEN,
287                "invalid O-format broker client order ID payload length"
288            );
289            let packed = decode_base62(data).context("invalid O-format broker client order ID")?;
290            Ok(unpack_o_format(packed, signal == SIGNAL_O_HYPHENS))
291        }
292        SIGNAL_UUID_HYPHENS | SIGNAL_UUID_NO_HYPHENS => {
293            anyhow::ensure!(
294                data.len() == UUID_B62_LEN,
295                "invalid UUID broker client order ID payload length"
296            );
297            let value = decode_base62(data).context("invalid UUID broker client order ID")?;
298            Ok(format_uuid(value, signal == SIGNAL_UUID_HYPHENS))
299        }
300        SIGNAL_RAW => {
301            let raw = std::str::from_utf8(data).context("invalid raw broker client order ID")?;
302            anyhow::ensure!(
303                !raw.is_empty(),
304                "missing raw broker client order ID payload"
305            );
306            Ok(raw.to_string())
307        }
308        _ => anyhow::bail!(
309            "unknown broker client order ID signal byte '{}'",
310            signal as char
311        ),
312    }
313}
314
315pub(crate) fn legacy_client_order_id(encoded: &str, broker_id: &str) -> Option<String> {
316    let packed = decode_base62(tagged_payload(encoded, broker_id)?)?;
317    unpack_o_tags(packed).ok()
318}
319
320fn tagged_payload<'a>(encoded: &'a str, broker_id: &str) -> Option<&'a [u8]> {
321    if encoded.len() != 3 + broker_id.len() + O_TAGS_B62_LEN
322        || encoded.len() >= MAX_CLIENT_ORDER_ID_LEN - (3 + broker_id.len())
323    {
324        return None;
325    }
326
327    let payload = encoded.strip_prefix("x-")?.strip_prefix(broker_id)?;
328    let (&signal, data) = payload.as_bytes().split_first()?;
329    (signal == SIGNAL_O_TAGS).then_some(data)
330}
331
332fn build_encoded(broker_id: &str, signal: u8, payload: &[u8]) -> String {
333    let tagged = signal == SIGNAL_O_TAGS;
334    let mut result =
335        String::with_capacity(3 + broker_id.len() + usize::from(!tagged) + payload.len());
336    result.push_str("x-");
337    result.push_str(broker_id);
338
339    if !tagged {
340        result.push('-');
341    }
342
343    result.push(signal as char);
344    result.push_str(std::str::from_utf8(payload).expect("encoded ID is valid UTF-8"));
345    result
346}
347
348fn encode_base62<const N: usize>(mut value: u128) -> [u8; N] {
349    const DIGITS: usize = 10;
350    const RADIX: u128 = 62u128.pow(DIGITS as u32);
351    let mut buf = [b'0'; N];
352    for chunk in buf.rchunks_mut(DIGITS) {
353        let mut part = (value % RADIX) as u64;
354        value /= RADIX;
355
356        for byte in chunk.iter_mut().rev() {
357            *byte = BASE62_CHARS[(part % 62) as usize];
358            part /= 62;
359        }
360    }
361    buf
362}
363
364fn decode_base62(encoded: &[u8]) -> Option<u128> {
365    let mut value: u128 = 0;
366
367    for &byte in encoded {
368        let digit = BASE62_DECODE[byte as usize & 0x7F];
369
370        if digit == 0xFF || !byte.is_ascii() {
371            return None;
372        }
373        value = value.checked_mul(62)?.checked_add(digit as u128)?;
374    }
375    Some(value)
376}
377
378fn parse_digits(bytes: &[u8]) -> Option<u32> {
379    let mut n: u32 = 0;
380
381    for &b in bytes {
382        if !b.is_ascii_digit() {
383            return None;
384        }
385        n = n * 10 + (b - b'0') as u32;
386    }
387    Some(n)
388}
389
390fn pack_o_format(id_str: &str) -> Option<(u128, bool)> {
391    let b = id_str.as_bytes();
392
393    if b.first() != Some(&b'O') {
394        return None;
395    }
396
397    let (year, month, day, hour, minute, second, trader, strategy, count, has_hyphens) =
398        if b.get(1) == Some(&b'-') {
399            // With hyphens: O-YYYYMMDD-HHMMSS-TTT-SSS-CCC
400            // Find hyphen positions manually to avoid Vec allocation
401            if b.len() < 23 || b[10] != b'-' || b[17] != b'-' {
402                return None;
403            }
404            let h4 = memchr_byte(b'-', &b[18..])?;
405            let trader_end = 18 + h4;
406            let h5 = memchr_byte(b'-', &b[trader_end + 1..])?;
407            let strategy_end = trader_end + 1 + h5;
408
409            (
410                parse_digits(&b[2..6])?,
411                parse_digits(&b[6..8])?,
412                parse_digits(&b[8..10])?,
413                parse_digits(&b[11..13])?,
414                parse_digits(&b[13..15])?,
415                parse_digits(&b[15..17])?,
416                parse_digits(&b[18..trader_end])?,
417                parse_digits(&b[trader_end + 1..strategy_end])?,
418                parse_digits(&b[strategy_end + 1..])?,
419                true,
420            )
421        } else {
422            // Without hyphens: OYYYYMMDDHHMMSSTTTSSSCC...
423            if b.len() < 22 {
424                return None;
425            }
426            (
427                parse_digits(&b[1..5])?,
428                parse_digits(&b[5..7])?,
429                parse_digits(&b[7..9])?,
430                parse_digits(&b[9..11])?,
431                parse_digits(&b[11..13])?,
432                parse_digits(&b[13..15])?,
433                parse_digits(&b[15..18])?,
434                parse_digits(&b[18..21])?,
435                parse_digits(&b[21..])?,
436                false,
437            )
438        };
439
440    if trader > 1023 || strategy > 1023 || count > 0xF_FFFF {
441        return None;
442    }
443
444    let secs_since_epoch = civil_to_epoch(year, month, day, hour, minute, second)? - O_FORMAT_EPOCH;
445
446    if secs_since_epoch < 0 {
447        return None;
448    }
449
450    let packed = (secs_since_epoch as u128) << 40
451        | (trader as u128) << 30
452        | (strategy as u128) << 20
453        | (count as u128);
454
455    Some((packed, has_hyphens))
456}
457
458fn unpack_o_format(packed: u128, has_hyphens: bool) -> String {
459    let count = (packed & 0xF_FFFF) as u32;
460    let strategy = ((packed >> 20) & 0x3FF) as u32;
461    let trader = ((packed >> 30) & 0x3FF) as u32;
462    let secs_since_epoch = (packed >> 40) as i64;
463
464    let timestamp = secs_since_epoch + O_FORMAT_EPOCH;
465    let Some((year, month, day, hour, minute, second)) = epoch_to_civil(timestamp) else {
466        log::warn!("Failed to decode O-format timestamp: {timestamp}");
467        return format!("DECODE_ERROR_{packed}");
468    };
469
470    let mut result = String::with_capacity(MAX_CLIENT_ORDER_ID_LEN);
471
472    if has_hyphens {
473        write!(result, "O-{year:04}{month:02}{day:02}-{hour:02}{minute:02}{second:02}-{trader:03}-{strategy:03}-{count}")
474            .expect("writing to String should not fail");
475    } else {
476        write!(result, "O{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}{trader:03}{strategy:03}{count}")
477            .expect("writing to String should not fail");
478    }
479
480    result
481}
482
483fn pack_o_tags(id: &str) -> Option<u128> {
484    let mut parts = id.split('-');
485    if parts.next()? != "O" {
486        return None;
487    }
488
489    let date = parts.next()?.as_bytes();
490    let time = parts.next()?.as_bytes();
491    let trader = parts.next()?;
492    let strategy = parts.next()?;
493    let count = parts.next()?;
494    if parts.next().is_some() || date.len() != 8 || time.len() != 6 {
495        return None;
496    }
497
498    let epoch = civil_to_epoch(
499        parse_digits(&date[..4])?,
500        parse_digits(&date[4..6])?,
501        parse_digits(&date[6..])?,
502        parse_digits(&time[..2])?,
503        parse_digits(&time[2..4])?,
504        parse_digits(&time[4..])?,
505    )?;
506    let seconds = u32::try_from(epoch - O_FORMAT_EPOCH).ok()?;
507    let count = count.parse::<u32>().ok()?;
508    if count > O_TAGS_COUNT_MAX {
509        return None;
510    }
511
512    for swapped in [false, true] {
513        let (tag, numeric) = if swapped {
514            (strategy, trader)
515        } else {
516            (trader, strategy)
517        };
518
519        let Some(tag) = pack_tag(tag) else {
520            continue;
521        };
522
523        let Ok(numeric) = numeric.parse::<u32>() else {
524            continue;
525        };
526
527        if numeric > 1023 {
528            continue;
529        }
530
531        let packed = ((seconds as u128) << 45)
532            | ((swapped as u128) << 44)
533            | ((tag as u128) << 32)
534            | ((numeric as u128) << O_TAGS_COUNT_BITS)
535            | count as u128;
536
537        if unpack_o_tags(packed).ok()?.as_str() == id {
538            return Some(packed);
539        }
540    }
541
542    None
543}
544
545fn unpack_o_tags(packed: u128) -> anyhow::Result<String> {
546    anyhow::ensure!(
547        packed >> 77 == 0,
548        "tagged O-format payload exceeds its bit budget"
549    );
550    let count = (packed & O_TAGS_COUNT_MAX as u128) as u32;
551    let numeric = ((packed >> O_TAGS_COUNT_BITS) & 0x3FF) as u32;
552    let tag = unpack_tag(((packed >> 32) & 0xFFF) as u32)?;
553    let swapped = ((packed >> 44) & 1) != 0;
554    let (year, month, day, hour, minute, second) =
555        epoch_to_civil((packed >> 45) as i64 + O_FORMAT_EPOCH)
556            .context("invalid tagged O-format timestamp")?;
557    let mut result = String::with_capacity(MAX_CLIENT_ORDER_ID_LEN);
558    write!(
559        result,
560        "O-{year:04}{month:02}{day:02}-{hour:02}{minute:02}{second:02}-"
561    )
562    .expect("writing to String should not fail");
563
564    if swapped {
565        write!(result, "{numeric:03}-{tag}-{count}").expect("writing to String should not fail");
566    } else {
567        write!(result, "{tag}-{numeric:03}-{count}").expect("writing to String should not fail");
568    }
569
570    Ok(result)
571}
572
573fn pack_tag(tag: &str) -> Option<u32> {
574    if !(1..=2).contains(&tag.len()) {
575        return None;
576    }
577
578    let mut value = 0;
579
580    for byte in tag.bytes() {
581        let digit = *BASE62_DECODE.get(byte as usize)?;
582        if digit == 0xFF {
583            return None;
584        }
585
586        value = value * 62 + u32::from(digit);
587    }
588
589    match tag.len() {
590        1 => Some(value + 1),
591        2 => Some(value + 63),
592        _ => None,
593    }
594}
595
596fn unpack_tag(value: u32) -> anyhow::Result<String> {
597    anyhow::ensure!((1..=3906).contains(&value), "invalid tagged O-format tag");
598    let mut tag = String::with_capacity(2);
599
600    if value <= 62 {
601        tag.push(BASE62_CHARS[(value - 1) as usize] as char);
602    } else {
603        let value = value - 63;
604        tag.push(BASE62_CHARS[(value / 62) as usize] as char);
605        tag.push(BASE62_CHARS[(value % 62) as usize] as char);
606    }
607
608    Ok(tag)
609}
610
611fn parse_uuid_hex(id_str: &str) -> Option<(u128, bool)> {
612    let b = id_str.as_bytes();
613
614    if b.len() == 36 && b[8] == b'-' {
615        // UUID with hyphens: 8-4-4-4-12
616        if b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
617            return None;
618        }
619        let mut value: u128 = 0;
620
621        for &byte in b {
622            if byte == b'-' {
623                continue;
624            }
625            let nibble = hex_digit(byte)?;
626            value = (value << 4) | nibble as u128;
627        }
628        Some((value, true))
629    } else if b.len() == 32 {
630        let mut value: u128 = 0;
631
632        for &byte in b {
633            let nibble = hex_digit(byte)?;
634            value = (value << 4) | nibble as u128;
635        }
636        Some((value, false))
637    } else {
638        None
639    }
640}
641
642fn format_uuid(value: u128, has_hyphens: bool) -> String {
643    const HEX: &[u8; 16] = b"0123456789abcdef";
644    let bytes = value.to_be_bytes();
645
646    if has_hyphens {
647        let mut buf = [0u8; 36];
648        let mut pos = 0;
649
650        for (i, &b) in bytes.iter().enumerate() {
651            if i == 4 || i == 6 || i == 8 || i == 10 {
652                buf[pos] = b'-';
653                pos += 1;
654            }
655            buf[pos] = HEX[(b >> 4) as usize];
656            buf[pos + 1] = HEX[(b & 0x0F) as usize];
657            pos += 2;
658        }
659        std::str::from_utf8(&buf)
660            .expect("hex is valid UTF-8")
661            .to_string()
662    } else {
663        let mut buf = [0u8; 32];
664        for (i, &b) in bytes.iter().enumerate() {
665            buf[i * 2] = HEX[(b >> 4) as usize];
666            buf[i * 2 + 1] = HEX[(b & 0x0F) as usize];
667        }
668        std::str::from_utf8(&buf)
669            .expect("hex is valid UTF-8")
670            .to_string()
671    }
672}
673
674fn hex_digit(byte: u8) -> Option<u8> {
675    match byte {
676        b'0'..=b'9' => Some(byte - b'0'),
677        b'a'..=b'f' => Some(byte - b'a' + 10),
678        b'A'..=b'F' => Some(byte - b'A' + 10),
679        _ => None,
680    }
681}
682
683fn memchr_byte(needle: u8, haystack: &[u8]) -> Option<usize> {
684    haystack.iter().position(|&b| b == needle)
685}
686
687/// Converts civil date/time to Unix timestamp (seconds since 1970-01-01).
688fn civil_to_epoch(year: u32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Option<i64> {
689    if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || min > 59 || sec > 59 {
690        return None;
691    }
692    // Days from civil date using the algorithm from Howard Hinnant
693    let y = if month <= 2 {
694        year as i64 - 1
695    } else {
696        year as i64
697    };
698    let era = y.div_euclid(400);
699    let yoe = y.rem_euclid(400) as u64;
700    let m = if month > 2 { month - 3 } else { month + 9 } as u64;
701    let doy = (153 * m + 2) / 5 + day as u64 - 1;
702    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
703    let days = era * 146097 + doe as i64 - 719468;
704    Some(days * 86400 + hour as i64 * 3600 + min as i64 * 60 + sec as i64)
705}
706
707/// Converts Unix timestamp to civil date/time components.
708fn epoch_to_civil(timestamp: i64) -> Option<(u32, u32, u32, u32, u32, u32)> {
709    if timestamp < 0 {
710        return None;
711    }
712    let secs_of_day = (timestamp % 86400) as u32;
713    let days = timestamp / 86400;
714
715    let hour = secs_of_day / 3600;
716    let minute = (secs_of_day % 3600) / 60;
717    let second = secs_of_day % 60;
718
719    // Civil date from day count using Howard Hinnant's algorithm
720    let z = days + 719468;
721    let era = z.div_euclid(146097);
722    let doe = z.rem_euclid(146097) as u64;
723    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
724    let y = yoe as i64 + era * 400;
725    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
726    let mp = (5 * doy + 2) / 153;
727    let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
728    let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
729    let year = if month <= 2 { y + 1 } else { y } as u32;
730
731    Some((year, month, day, hour, minute, second))
732}
733
734#[cfg(test)]
735mod tests {
736    use std::hint::black_box;
737
738    use rstest::rstest;
739
740    use super::{super::consts::BINANCE_NAUTILUS_SPOT_BROKER_ID, *};
741
742    const TEST_BROKER_ID: &str = BINANCE_NAUTILUS_SPOT_BROKER_ID;
743
744    #[rstest]
745    #[case("O-20260922-160119-V2-000-8")]
746    #[case("O-20260922-200130-V2-001-24")]
747    #[case("O-20260923-000138-V2-001-1")]
748    #[case("O-20260923-000138-V2-001-4194303")]
749    #[case("O-20260923-000138-001-aZ-42")]
750    fn test_tagged_id_preserves_exact_identity(#[case] original: &str) {
751        let id = ClientOrderId::from(original);
752        let encoded = encode_broker_id(&id, TEST_BROKER_ID);
753        assert_eq!(encoded.len(), 24);
754        assert!(encoded.starts_with("x-TD67BGP9A"));
755        assert_eq!(
756            decode_client_order_id(&encoded, TEST_BROKER_ID).unwrap(),
757            id
758        );
759
760        // Historical custom IDs matching this format were R-wrapped
761        let custom = ClientOrderId::from(encoded.as_str());
762        let wrapped = encode_broker_id(&custom, TEST_BROKER_ID);
763        assert_eq!(wrapped, format!("x-TD67BGP9-R{encoded}"));
764        assert_eq!(
765            decode_client_order_id(&wrapped, TEST_BROKER_ID).unwrap(),
766            custom
767        );
768    }
769
770    #[rstest]
771    #[case("O-20260923-000138-V2-001-4194304")]
772    #[case("O-20260923-000138-LONG-001-1")]
773    #[case("O-20260923-000138-V2-001-01")]
774    #[case("O-20260230-000138-V2-001-1")]
775    fn test_unsupported_tagged_id_remains_unchanged(#[case] original: &str) {
776        assert_eq!(
777            encode_broker_id(&ClientOrderId::from(original), TEST_BROKER_ID),
778            original
779        );
780    }
781
782    #[rstest]
783    fn test_historical_raw_id_does_not_enter_short_namespace() {
784        let original = "x-TD67BGP9A00000000000000";
785        assert_eq!(original.len(), 25);
786        assert_eq!(
787            encode_broker_id(&ClientOrderId::from(original), TEST_BROKER_ID),
788            original
789        );
790        assert_eq!(
791            decode_client_order_id(original, TEST_BROKER_ID)
792                .unwrap()
793                .as_str(),
794            original
795        );
796    }
797
798    #[rstest]
799    #[case("O-20260305-120000-001-001-100", "x-TD67BGP9-T047IBdyLevrb2")]
800    #[case("O20260305120000001001100", "x-TD67BGP9-t047IBdyLevrb2")]
801    #[case(
802        "550e8400-e29b-41d4-a716-446655440000",
803        "x-TD67BGP9-U2aUyqjCzEIiEcYMKj7TZtw"
804    )]
805    #[case(
806        "550e8400e29b41d4a716446655440000",
807        "x-TD67BGP9-u2aUyqjCzEIiEcYMKj7TZtw"
808    )]
809    #[case("my-order-123", "x-TD67BGP9-Rmy-order-123")]
810    fn test_historical_wire_vectors(#[case] original: &str, #[case] encoded: &str) {
811        let id = ClientOrderId::from(original);
812        assert_eq!(encode_broker_id(&id, TEST_BROKER_ID), encoded);
813        assert_eq!(decode_client_order_id(encoded, TEST_BROKER_ID).unwrap(), id);
814    }
815
816    #[rstest]
817    fn test_base62_chunked_encoding_matches_digit_reference() {
818        let mut state = 0x1234_5678_90ab_cdef_2345_6789_abcd_ef01u128;
819
820        for value in [0, 1, 61, 62, 62u128.pow(10) - 1, 62u128.pow(10), u128::MAX]
821            .into_iter()
822            .chain((0..10_000).map(|_| {
823                state ^= state << 13;
824                state ^= state >> 7;
825                state ^= state << 17;
826                state
827            }))
828        {
829            let mut expected = [b'0'; 22];
830            let mut remaining = value;
831
832            for byte in expected.iter_mut().rev() {
833                *byte = BASE62_CHARS[(remaining % 62) as usize];
834                remaining /= 62;
835            }
836
837            assert_eq!(encode_base62::<22>(value), expected);
838            assert_eq!(encode_base62::<13>(value).as_slice(), &expected[9..]);
839            assert_eq!(decode_base62(&expected), Some(value));
840        }
841    }
842
843    #[rstest]
844    #[case("O-20200101-000000-0-000-0")]
845    #[case("O-21560207-062815-zz-1023-4194303")]
846    #[case("O-20260922-160119-000-00-1")]
847    fn test_tagged_packing_boundaries(#[case] original: &str) {
848        let packed = pack_o_tags(original).unwrap();
849        assert_eq!(unpack_o_tags(packed).unwrap(), original);
850    }
851
852    #[rstest]
853    fn test_base62_roundtrip_zero() {
854        let encoded = encode_base62::<13>(0);
855        let decoded = decode_base62(&encoded);
856        assert_eq!(decoded, Some(0));
857    }
858
859    #[rstest]
860    fn test_base62_roundtrip_max_72_bit() {
861        let value: u128 = (1u128 << 72) - 1;
862        let encoded = encode_base62::<13>(value);
863        let decoded = decode_base62(&encoded);
864        assert_eq!(decoded, Some(value));
865    }
866
867    #[rstest]
868    fn test_base62_roundtrip_max_128_bit() {
869        let value: u128 = u128::MAX;
870        let encoded = encode_base62::<22>(value);
871        let decoded = decode_base62(&encoded);
872        assert_eq!(decoded, Some(value));
873    }
874
875    #[rstest]
876    #[case("O-20200101-000000-000-000-0")]
877    #[case("O-20200101-000001-001-001-1")]
878    #[case("O-20260131-174827-001-001-1")]
879    #[case("O-20260131-235959-999-999-4095")]
880    #[case("O-20251215-123456-123-456-789")]
881    #[case("O-20260305-120000-001-001-100")]
882    #[case("O-20260305-120000-001-001-99999")]
883    #[case("O-20260305-120000-001-001-1048575")]
884    fn test_roundtrip_o_format_with_hyphens(#[case] id_str: &str) {
885        let coid = ClientOrderId::from(id_str);
886        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
887
888        assert!(encoded.starts_with("x-TD67BGP9-T"), "got: {encoded}");
889        assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
890
891        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
892        assert_eq!(decoded, id_str);
893    }
894
895    #[rstest]
896    #[case("O202001010000000000000")]
897    #[case("O202601311748270010011")]
898    #[case("O202601312359599999994095")]
899    fn test_roundtrip_o_format_without_hyphens(#[case] id_str: &str) {
900        let coid = ClientOrderId::from(id_str);
901        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
902
903        assert!(encoded.starts_with("x-TD67BGP9-t"), "got: {encoded}");
904        assert!(encoded.len() <= 36);
905
906        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
907        assert_eq!(decoded, id_str);
908    }
909
910    #[rstest]
911    fn test_roundtrip_uuid_with_hyphens() {
912        let id_str = "550e8400-e29b-41d4-a716-446655440000";
913        let coid = ClientOrderId::from(id_str);
914        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
915
916        assert!(encoded.starts_with("x-TD67BGP9-U"), "got: {encoded}");
917        assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
918
919        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
920        assert_eq!(decoded, id_str);
921    }
922
923    #[rstest]
924    fn test_roundtrip_uuid_without_hyphens() {
925        let id_str = "550e8400e29b41d4a716446655440000";
926        let coid = ClientOrderId::from(id_str);
927        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
928
929        assert!(encoded.starts_with("x-TD67BGP9-u"), "got: {encoded}");
930        assert!(encoded.len() <= 36);
931
932        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
933        assert_eq!(decoded, id_str);
934    }
935
936    #[rstest]
937    fn test_roundtrip_uuid_all_zeros() {
938        let id_str = "00000000-0000-0000-0000-000000000000";
939        let coid = ClientOrderId::from(id_str);
940
941        let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
942        assert_eq!(decoded, id_str);
943    }
944
945    #[rstest]
946    fn test_roundtrip_uuid_all_f() {
947        let id_str = "ffffffff-ffff-ffff-ffff-ffffffffffff";
948        let coid = ClientOrderId::from(id_str);
949
950        let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
951        assert_eq!(decoded, id_str);
952    }
953
954    #[rstest]
955    fn test_raw_passthrough_short_id() {
956        let id_str = "my-order-123";
957        let coid = ClientOrderId::from(id_str);
958        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
959
960        assert!(encoded.starts_with("x-TD67BGP9-R"), "got: {encoded}");
961        assert!(encoded.len() <= 36);
962
963        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
964        assert_eq!(decoded, id_str);
965    }
966
967    #[rstest]
968    fn test_raw_passthrough_max_length() {
969        let id_str = "abcdefghijklmnopqrstuvwx"; // 24 chars = max raw budget
970        let coid = ClientOrderId::from(id_str);
971        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
972
973        assert_eq!(encoded.len(), 36);
974        assert!(encoded.starts_with("x-TD67BGP9-R"));
975
976        let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
977        assert_eq!(decoded, id_str);
978    }
979
980    #[rstest]
981    fn test_decode_non_prefixed_returns_as_is() {
982        let raw = "O-20260131-174827-001-001-1";
983        assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
984    }
985
986    #[rstest]
987    fn test_decode_different_prefix_returns_as_is() {
988        let raw = "x-OTHERBROKER-T0000000000000";
989        assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
990    }
991
992    #[rstest]
993    #[case::empty("", "invalid Binance client order ID ''")]
994    #[case::whitespace("   ", "invalid Binance client order ID '   '")]
995    #[case::non_ascii("client-é", "invalid Binance client order ID 'client-é'")]
996    #[case::missing_signal("x-TD67BGP9-", "missing broker client order ID signal")]
997    #[case::missing_raw_payload("x-TD67BGP9-R", "missing raw broker client order ID payload")]
998    #[case::invalid_o_payload(
999        "x-TD67BGP9-T000000000000!",
1000        "invalid O-format broker client order ID"
1001    )]
1002    #[case::unknown_signal(
1003        "x-TD67BGP9-Xlegacy-order",
1004        "unknown broker client order ID signal byte 'X'"
1005    )]
1006    fn test_decode_client_order_id_rejects_invalid_input(
1007        #[case] encoded: &str,
1008        #[case] expected: &str,
1009    ) {
1010        let error = decode_client_order_id(encoded, TEST_BROKER_ID).unwrap_err();
1011
1012        assert_eq!(error.to_string(), expected);
1013    }
1014
1015    #[rstest]
1016    fn test_decode_client_order_id_preserves_valid_prefixed_id() {
1017        let original = ClientOrderId::from("O-20260305-120000-001-001-100");
1018        let encoded = encode_broker_id(&original, TEST_BROKER_ID);
1019
1020        let decoded = decode_client_order_id(&encoded, TEST_BROKER_ID).unwrap();
1021
1022        assert_eq!(decoded, original);
1023    }
1024
1025    #[rstest]
1026    fn test_decode_client_order_id_preserves_valid_legacy_id() {
1027        let decoded = decode_client_order_id("legacy-order-1", TEST_BROKER_ID).unwrap();
1028
1029        assert_eq!(decoded, ClientOrderId::from("legacy-order-1"));
1030    }
1031
1032    #[rstest]
1033    fn test_o_format_trader_overflow_sends_without_prefix() {
1034        // trader=1024 exceeds 10-bit limit, and hyphenated O-format (28 chars)
1035        // exceeds raw budget too, so the ID is sent without prefix
1036        let id_str = "O-20260131-174827-1024-001-1";
1037        let coid = ClientOrderId::from(id_str);
1038        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1039        assert_eq!(encoded, id_str);
1040    }
1041
1042    #[rstest]
1043    fn test_o_format_count_overflow_sends_without_prefix() {
1044        // count=1048576 exceeds 20-bit limit, and hyphenated O-format (32 chars)
1045        // exceeds raw budget too, so the ID is sent without prefix
1046        let id_str = "O-20260131-174827-001-001-1048576";
1047        let coid = ClientOrderId::from(id_str);
1048        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1049        assert_eq!(encoded, id_str);
1050    }
1051
1052    #[rstest]
1053    fn test_too_long_id_sends_without_prefix() {
1054        let id_str = "this-is-a-very-long-order-id-that-exceeds-everything";
1055        let coid = ClientOrderId::from(id_str);
1056        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1057
1058        assert_eq!(encoded, id_str);
1059    }
1060
1061    #[rstest]
1062    fn test_o_format_always_25_chars() {
1063        let test_cases = [
1064            "O-20200101-000000-000-000-0",
1065            "O-20260131-235959-999-999-4095",
1066            "O-20260305-120000-001-001-1048575",
1067        ];
1068
1069        for id_str in test_cases {
1070            let coid = ClientOrderId::from(id_str);
1071            let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1072            assert_eq!(
1073                encoded.len(),
1074                25,
1075                "got {} for {id_str}: {encoded}",
1076                encoded.len()
1077            );
1078        }
1079    }
1080
1081    #[rstest]
1082    fn test_uuid_always_34_chars() {
1083        let id_str = "550e8400-e29b-41d4-a716-446655440000";
1084        let coid = ClientOrderId::from(id_str);
1085        let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1086        assert_eq!(encoded.len(), 34, "got {}", encoded.len());
1087    }
1088
1089    #[rstest]
1090    fn test_broker_prefix_format() {
1091        let encoded = encode_broker_id(&ClientOrderId::from("test"), TEST_BROKER_ID);
1092        assert_eq!(encoded, "x-TD67BGP9-Rtest");
1093    }
1094
1095    #[rstest]
1096    fn test_encoded_chars_are_binance_valid() {
1097        let valid = |c: char| {
1098            c.is_ascii_alphanumeric() || c == '.' || c == ':' || c == '/' || c == '_' || c == '-'
1099        };
1100
1101        let ids = [
1102            "O-20260131-174827-001-001-1",
1103            "550e8400-e29b-41d4-a716-446655440000",
1104            "short-id",
1105        ];
1106
1107        for id_str in ids {
1108            let coid = ClientOrderId::from(id_str);
1109            let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1110            assert!(
1111                encoded.chars().all(valid),
1112                "'{encoded}' contains invalid Binance characters"
1113            );
1114        }
1115    }
1116
1117    #[rstest]
1118    fn test_civil_time_roundtrip() {
1119        let epoch = civil_to_epoch(2020, 1, 1, 0, 0, 0).unwrap();
1120        assert_eq!(epoch, O_FORMAT_EPOCH);
1121        let (y, m, d, h, mi, s) = epoch_to_civil(epoch).unwrap();
1122        assert_eq!((y, m, d, h, mi, s), (2020, 1, 1, 0, 0, 0));
1123    }
1124
1125    #[rstest]
1126    #[case("O-20260305-120000-001-001-1")]
1127    #[case("O-20260131-174827-001-001-1")]
1128    #[case("O-20260305-120000-001-001-1048575")]
1129    #[case("550e8400-e29b-41d4-a716-446655440000")]
1130    #[case("550e8400e29b41d4a716446655440000")]
1131    #[case("my-order-42")]
1132    #[case("short")]
1133    fn test_end_to_end_submit_and_receive(#[case] original_id: &str) {
1134        let broker_id = TEST_BROKER_ID;
1135        let client_order_id = ClientOrderId::from(original_id);
1136
1137        // Simulate submit: encode the client order ID for Binance
1138        let encoded = encode_broker_id(&client_order_id, broker_id);
1139        assert!(encoded.len() <= 36, "encoded len {} > 36", encoded.len());
1140
1141        // Simulate receive: Binance echoes the encoded ID back in a response
1142        let decoded = decode_broker_id(&encoded, broker_id);
1143
1144        // Must recover the original ID exactly
1145        assert_eq!(decoded, original_id);
1146        assert_eq!(ClientOrderId::new(decoded), client_order_id);
1147    }
1148
1149    #[rstest]
1150    fn bench_encode_decode_timing() {
1151        let o_coid = ClientOrderId::from("O-20260305-120000-001-001-100");
1152        let uuid_coid = ClientOrderId::from("550e8400-e29b-41d4-a716-446655440000");
1153        let raw_coid = ClientOrderId::from("my-order-123");
1154
1155        let iterations = 100_000;
1156
1157        let start = std::time::Instant::now();
1158
1159        for _ in 0..iterations {
1160            black_box(encode_broker_id(black_box(&o_coid), TEST_BROKER_ID));
1161        }
1162        let encode_o = start.elapsed();
1163
1164        let o_encoded = encode_broker_id(&o_coid, TEST_BROKER_ID);
1165        let start = std::time::Instant::now();
1166
1167        for _ in 0..iterations {
1168            black_box(decode_broker_id(black_box(&o_encoded), TEST_BROKER_ID));
1169        }
1170        let decode_o = start.elapsed();
1171
1172        let start = std::time::Instant::now();
1173
1174        for _ in 0..iterations {
1175            black_box(encode_broker_id(black_box(&uuid_coid), TEST_BROKER_ID));
1176        }
1177        let encode_uuid = start.elapsed();
1178
1179        let uuid_encoded = encode_broker_id(&uuid_coid, TEST_BROKER_ID);
1180        let start = std::time::Instant::now();
1181
1182        for _ in 0..iterations {
1183            black_box(decode_broker_id(black_box(&uuid_encoded), TEST_BROKER_ID));
1184        }
1185        let decode_uuid = start.elapsed();
1186
1187        let start = std::time::Instant::now();
1188
1189        for _ in 0..iterations {
1190            black_box(encode_broker_id(black_box(&raw_coid), TEST_BROKER_ID));
1191        }
1192        let encode_raw = start.elapsed();
1193
1194        let raw_encoded = encode_broker_id(&raw_coid, TEST_BROKER_ID);
1195        let start = std::time::Instant::now();
1196
1197        for _ in 0..iterations {
1198            black_box(decode_broker_id(black_box(&raw_encoded), TEST_BROKER_ID));
1199        }
1200        let decode_raw = start.elapsed();
1201
1202        let passthrough = "O-20260305-120000-001-001-100";
1203        let start = std::time::Instant::now();
1204
1205        for _ in 0..iterations {
1206            black_box(decode_broker_id(black_box(passthrough), TEST_BROKER_ID));
1207        }
1208        let decode_pass = start.elapsed();
1209
1210        println!("\n--- Broker ID Encoder Performance ({iterations} iterations) ---");
1211        println!(
1212            "encode O-format:     {:>8.1} ns/op",
1213            encode_o.as_nanos() as f64 / iterations as f64
1214        );
1215        println!(
1216            "decode O-format:     {:>8.1} ns/op",
1217            decode_o.as_nanos() as f64 / iterations as f64
1218        );
1219        println!(
1220            "encode UUID:         {:>8.1} ns/op",
1221            encode_uuid.as_nanos() as f64 / iterations as f64
1222        );
1223        println!(
1224            "decode UUID:         {:>8.1} ns/op",
1225            decode_uuid.as_nanos() as f64 / iterations as f64
1226        );
1227        println!(
1228            "encode raw:          {:>8.1} ns/op",
1229            encode_raw.as_nanos() as f64 / iterations as f64
1230        );
1231        println!(
1232            "decode raw:          {:>8.1} ns/op",
1233            decode_raw.as_nanos() as f64 / iterations as f64
1234        );
1235        println!(
1236            "decode passthrough:  {:>8.1} ns/op",
1237            decode_pass.as_nanos() as f64 / iterations as f64
1238        );
1239    }
1240}