Skip to content

Commit a704128

Browse files
committed
Optimize Date display impl
1 parent 6efd854 commit a704128

4 files changed

Lines changed: 206 additions & 88 deletions

File tree

time/src/date.rs

Lines changed: 90 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22
33
#[cfg(feature = "formatting")]
44
use alloc::string::String;
5+
use core::fmt;
6+
use core::mem::MaybeUninit;
57
use core::num::NonZero;
68
use core::ops::{Add, AddAssign, Sub, SubAssign};
79
use core::time::Duration as StdDuration;
8-
use core::{cmp, fmt};
910
#[cfg(feature = "formatting")]
1011
use std::io;
1112

1213
use deranged::RangedI32;
1314
use num_conv::prelude::*;
14-
use powerfmt::ext::FormatterExt;
15-
use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay};
15+
use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay};
1616

17-
use crate::ext::DigitCount;
1817
#[cfg(feature = "formatting")]
1918
use crate::formatting::Formattable;
2019
use crate::internal_macros::{const_try, const_try_opt, div_floor, ensure_ranged};
20+
use crate::num_fmt::{four_to_six_digits, str_from_raw_parts, two_digits_zero_padded};
2121
#[cfg(feature = "parsing")]
2222
use crate::parsing::Parsable;
2323
use crate::unit::*;
@@ -1417,90 +1417,111 @@ impl Date {
14171417
mod private {
14181418
/// Metadata for `Date`.
14191419
#[non_exhaustive]
1420-
#[derive(Debug, Clone, Copy)]
1421-
pub struct DateMetadata {
1422-
/// The width of the year component, including the sign.
1423-
pub(super) year_width: u8,
1424-
/// Whether the sign should be displayed.
1425-
pub(super) display_sign: bool,
1426-
pub(super) year: i32,
1427-
pub(super) month: u8,
1428-
pub(super) day: u8,
1429-
}
1420+
#[derive(Debug)]
1421+
pub struct DateMetadata;
14301422
}
14311423
use private::DateMetadata;
14321424

1425+
// This no longer needs special handling, as the format is fixed and doesn't require anything
1426+
// advanced. Trait impls can't be deprecated and the info is still useful for other types
1427+
// implementing `SmartDisplay`, so leave it as-is for now.
14331428
impl SmartDisplay for Date {
14341429
type Metadata = DateMetadata;
14351430

14361431
#[inline]
14371432
fn metadata(&self, _: FormatterOptions) -> Metadata<'_, Self> {
1438-
let (year, month, day) = self.to_calendar_date();
1439-
1440-
// There is a minimum of four digits for any year.
1441-
let mut year_width = cmp::max(year.unsigned_abs().num_digits(), 4);
1442-
let display_sign = if !(0..10_000).contains(&year) {
1443-
// An extra character is required for the sign.
1444-
year_width += 1;
1445-
true
1446-
} else {
1447-
false
1448-
};
1433+
use crate::ext::DigitCount as _;
14491434

1450-
let formatted_width = year_width.extend::<usize>()
1451-
+ smart_display::padded_width_of!(
1452-
"-",
1453-
u8::from(month) => width(2),
1454-
"-",
1455-
day => width(2),
1456-
);
1435+
let year_sign_width =
1436+
if self.year() < 0 || (cfg!(feature = "large-dates") && self.year() >= 10_000) {
1437+
1
1438+
} else {
1439+
0
1440+
};
1441+
let year_width = self.year().unsigned_abs().num_digits().clamp(4, 6);
1442+
let formatted_width = year_sign_width + year_width + 6; // include two dashes and two digits each for month and day
14571443

1458-
Metadata::new(
1459-
formatted_width,
1460-
self,
1461-
DateMetadata {
1462-
year_width,
1463-
display_sign,
1464-
year,
1465-
month: u8::from(month),
1466-
day,
1467-
},
1468-
)
1444+
Metadata::new(formatted_width as usize, self, DateMetadata)
14691445
}
14701446

14711447
#[inline]
1472-
fn fmt_with_metadata(
1473-
&self,
1474-
f: &mut fmt::Formatter<'_>,
1475-
metadata: Metadata<Self>,
1476-
) -> fmt::Result {
1477-
let DateMetadata {
1478-
year_width,
1479-
display_sign,
1480-
year,
1481-
month,
1482-
day,
1483-
} = *metadata;
1484-
let year_width = year_width.extend();
1485-
1486-
if display_sign {
1487-
f.pad_with_width(
1488-
metadata.unpadded_width(),
1489-
format_args!("{year:+0year_width$}-{month:02}-{day:02}"),
1490-
)
1491-
} else {
1492-
f.pad_with_width(
1493-
metadata.unpadded_width(),
1494-
format_args!("{year:0year_width$}-{month:02}-{day:02}"),
1495-
)
1496-
}
1448+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1449+
fmt::Display::fmt(self, f)
14971450
}
14981451
}
14991452

15001453
impl fmt::Display for Date {
15011454
#[inline]
15021455
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1503-
SmartDisplay::fmt(self, f)
1456+
let mut buf = [MaybeUninit::uninit(); 13];
1457+
let mut idx = 0;
1458+
let (year, month, day) = self.to_calendar_date();
1459+
1460+
// Compute the sign of the integer, if any. Doing this in a branchless manner gives a
1461+
// significant performance improvement.
1462+
let neg = year.is_negative() as u8;
1463+
let pos = (cfg!(feature = "large-dates") && year - 10_000 >= 0) as u8;
1464+
let sign = b'+' + 2 * neg; // b'-' if `neg` is true, b'+' otherwise
1465+
// Always write the computed byte, even if it's later overwritten by the first digit of the
1466+
// year.
1467+
buf[idx] = MaybeUninit::new(sign);
1468+
idx += (neg | pos) as usize;
1469+
1470+
// Safety: `year.unsigned_abs()` is less than 1,000,000.
1471+
let [first_two, second_two, third_two] = unsafe { four_to_six_digits(year.unsigned_abs()) };
1472+
// Safety:
1473+
// - both `first_two` and `buf` are valid for reads and writes of up to 2 bytes.
1474+
// - `u8` is 1-aligned, so that is not a concern.
1475+
// - `first_two` points to static memory, while `buf` is a local variable, so they do not
1476+
// overlap.
1477+
unsafe {
1478+
first_two
1479+
.as_ptr()
1480+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(idx).cast(), first_two.len());
1481+
}
1482+
idx += first_two.len();
1483+
// Safety: See above.
1484+
unsafe {
1485+
second_two
1486+
.as_ptr()
1487+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(idx).cast(), 2);
1488+
}
1489+
idx += 2;
1490+
// Safety: See above.
1491+
unsafe {
1492+
third_two
1493+
.as_ptr()
1494+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(idx).cast(), 2);
1495+
}
1496+
idx += 2;
1497+
1498+
buf[idx] = MaybeUninit::new(b'-');
1499+
idx += 1;
1500+
1501+
// Safety: See above for `copy_to_nonoverlapping`. `two_digits` is valid because `month` is
1502+
// in the range 1..=12.
1503+
unsafe {
1504+
two_digits_zero_padded(u8::from(month))
1505+
.as_ptr()
1506+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(idx).cast(), 2);
1507+
}
1508+
idx += 2;
1509+
1510+
buf[idx] = MaybeUninit::new(b'-');
1511+
idx += 1;
1512+
1513+
// Safety: See above for `copy_to_nonoverlapping`. `two_digits` is valid because `day` is in
1514+
// the range 1..=31.
1515+
unsafe {
1516+
two_digits_zero_padded(day)
1517+
.as_ptr()
1518+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(idx).cast(), 2);
1519+
}
1520+
idx += 2;
1521+
1522+
// Safety: All bytes up to `idx` have been initialized with ASCII characters.
1523+
let s = unsafe { str_from_raw_parts((&raw const buf).cast(), idx) };
1524+
f.pad(s)
15041525
}
15051526
}
15061527

time/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ mod interop;
114114
#[cfg(feature = "macros")]
115115
pub mod macros;
116116
mod month;
117+
mod num_fmt;
117118
mod offset_date_time;
118119
#[cfg(feature = "parsing")]
119120
pub mod parsing;

time/src/num_fmt.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! Formatting utilities for numbers.
2+
//!
3+
//! These functions are low-level, but are designed to be _extremely_ fast for their designed use
4+
//! cases. They are `unsafe` to avoid validating input, have strict requirements, and may not return
5+
//! the most ergonomic types to avoid unnecessary allocations and copying.
6+
7+
use core::{hint, slice};
8+
9+
static ZERO_PADDED_PAIRS: [u8; 200] = *b"0001020304050607080910111213141516171819\
10+
2021222324252627282930313233343536373839\
11+
4041424344454647484950515253545556575859\
12+
6061626364656667686970717273747576777879\
13+
8081828384858687888990919293949596979899";
14+
15+
static SPACE_PADDED_PAIRS: [u8; 200] = *b" 0 1 2 3 4 5 6 7 8 910111213141516171819\
16+
2021222324252627282930313233343536373839\
17+
4041424344454647484950515253545556575859\
18+
6061626364656667686970717273747576777879\
19+
8081828384858687888990919293949596979899";
20+
21+
/// Safety:
22+
///
23+
/// - `ptr` must be non-null and point to `len` initialized bytes of UTF-8 data.
24+
/// - `ptr` is valid for (and not mutated during) lifetime `'a`.
25+
#[inline]
26+
pub(crate) const unsafe fn str_from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a str {
27+
// Safety: The caller must ensure that `ptr` is valid for `len` bytes and that the bytes are
28+
// valid UTF-8. The caller must also ensure that the lifetime `'a` is valid for the returned
29+
// string.
30+
unsafe { str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) }
31+
}
32+
33+
/// Obtain a string of two ASCII digits representing `n`. This includes a leading zero if `n` is
34+
/// less than 10.
35+
///
36+
/// # Safety: `n` must be less than 100.
37+
#[inline]
38+
pub(crate) const unsafe fn two_digits_zero_padded(n: u8) -> &'static str {
39+
debug_assert!(n < 100);
40+
41+
// Safety: We're staying within the bounds of the array. The array contains only ASCII
42+
// characters, so it's valid UTF-8.
43+
unsafe { str_from_raw_parts(ZERO_PADDED_PAIRS.as_ptr().add((n as usize) * 2), 2) }
44+
}
45+
46+
/// Obtain a string of two ASCII digits representing `n`. This includes a leading space if `n` is
47+
/// less than 10.
48+
///
49+
/// # Safety: `n` must be less than 100.
50+
#[expect(dead_code, reason = "likely to be used in the future")]
51+
#[inline]
52+
pub(crate) const unsafe fn two_digits_space_padded(n: u8) -> &'static str {
53+
debug_assert!(n < 100);
54+
55+
// Safety: We're staying within the bounds of the array. The array contains only ASCII
56+
// characters, so it's valid UTF-8.
57+
unsafe { str_from_raw_parts(SPACE_PADDED_PAIRS.as_ptr().add((n as usize) * 2), 2) }
58+
}
59+
60+
/// Obtain two strings of two ASCII digits each representing `n`. The first string is the most
61+
/// significant. Leading zeros are included if the number has fewer than 4 digits.
62+
///
63+
/// # Safety: `n` must be less than 10,000.
64+
#[inline]
65+
pub(crate) const unsafe fn four_digits(n: u16) -> [&'static str; 2] {
66+
debug_assert!(n < 10_000);
67+
68+
const EXP: u32 = 19; // 19 is faster or equal to 12 even for 3 digits.
69+
const SIG: u32 = (1 << EXP) / 100 + 1;
70+
71+
let high = (n as u32 * SIG) >> EXP; // value / 100
72+
let low = n as u32 - high * 100;
73+
74+
// Safety: We're staying within the bounds of the array.
75+
unsafe {
76+
[
77+
two_digits_zero_padded(high as u8),
78+
two_digits_zero_padded(low as u8),
79+
]
80+
}
81+
}
82+
83+
/// Obtain three strings which together represent `n`. The first string is the most significant.
84+
/// Leading zeros are included if the number has fewer than 4 digits. The first string will be empty
85+
/// if `n` is less than 10,000.
86+
///
87+
/// # Safety: `n` must be less than 1,000,000.
88+
#[inline]
89+
pub(crate) const unsafe fn four_to_six_digits(n: u32) -> [&'static str; 3] {
90+
debug_assert!(n < 1_000_000);
91+
// Safety: The caller must ensure that this is true.
92+
unsafe { hint::assert_unchecked(n < 1_000_000) };
93+
94+
let (first_two, remaining) = (n / 10_000, n % 10_000);
95+
96+
let size = 2 - (first_two < 10) as usize - (first_two == 0) as usize;
97+
let offset = first_two as usize * 2 + 2 - size;
98+
99+
// Safety: `offset` is within the bounds of the array. The array contains only ASCII characters,
100+
// so it's valid UTF-8.
101+
let first_two = unsafe { str_from_raw_parts(ZERO_PADDED_PAIRS.as_ptr().add(offset), size) };
102+
// Safety: `remaining` is guaranteed to be less than 10,000 due to the modulus above.
103+
let [second_two, last_two] = unsafe { four_digits(remaining as u16) };
104+
[first_two, second_two, last_two]
105+
}

time/src/utc_offset.rs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::error;
1818
#[cfg(feature = "formatting")]
1919
use crate::formatting::Formattable;
2020
use crate::internal_macros::ensure_ranged;
21+
use crate::num_fmt::two_digits_zero_padded;
2122
#[cfg(feature = "parsing")]
2223
use crate::parsing::Parsable;
2324
#[cfg(feature = "local-offset")]
@@ -534,22 +535,6 @@ impl SmartDisplay for UtcOffset {
534535
}
535536
}
536537

537-
/// Obtain a pointer to the two ASCII digits representing `n`.
538-
///
539-
/// # Safety: `n` must be less than 100.
540-
#[inline]
541-
unsafe fn digits_ptr(n: u8) -> *const u8 {
542-
const DIGIT_PAIRS: [u8; 200] = *b"0001020304050607080910111213141516171819\
543-
2021222324252627282930313233343536373839\
544-
4041424344454647484950515253545556575859\
545-
6061626364656667686970717273747576777879\
546-
8081828384858687888990919293949596979899";
547-
548-
debug_assert!(n < 100);
549-
// Safety: We're staying within the bounds of the array.
550-
unsafe { DIGIT_PAIRS.as_ptr().add((n as usize) * 2) }
551-
}
552-
553538
impl fmt::Display for UtcOffset {
554539
#[inline]
555540
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -563,9 +548,15 @@ impl fmt::Display for UtcOffset {
563548
// Safety: `hours`, `minutes` and `seconds` are all less than 100. Both the source and
564549
// destination are valid for two bytes, aligned, and do not overlap.
565550
unsafe {
566-
digits_ptr(hours).copy_to_nonoverlapping(buf.as_mut_ptr().add(1), 2);
567-
digits_ptr(minutes).copy_to_nonoverlapping(buf.as_mut_ptr().add(4), 2);
568-
digits_ptr(seconds).copy_to_nonoverlapping(buf.as_mut_ptr().add(7), 2);
551+
two_digits_zero_padded(hours)
552+
.as_ptr()
553+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(1), 2);
554+
two_digits_zero_padded(minutes)
555+
.as_ptr()
556+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(4), 2);
557+
two_digits_zero_padded(seconds)
558+
.as_ptr()
559+
.copy_to_nonoverlapping(buf.as_mut_ptr().add(7), 2);
569560
}
570561

571562
// Safety: All bytes are ASCII, which is a subset of UTF-8.

0 commit comments

Comments
 (0)