Lesson 2 of 6
Dates, Regex & Everyday Crates
Rust's standard library is deliberately small. Things that many languages build in, such as calendar dates, regular expressions, and random numbers, come from crates. They're not second-class: the crates in this lesson are maintained by core community members, and some are officially part of the rust-lang organization.
| Task | Rust |
|---|---|
| Measure elapsed time | std::time::Instant, Duration |
| The current wall-clock time | SystemTime::now(), chrono::Utc::now() |
| A date or date-time without a time zone | chrono::NaiveDate, NaiveDateTime |
| An exact moment in UTC | chrono::DateTime<Utc> |
| A moment with a fixed UTC offset | chrono::DateTime<FixedOffset> |
| Format and parse dates | format("%d/%m/%Y"), parse_from_str(s, "%d/%m/%Y") |
| Regular expressions | regex::Regex |
| Random numbers | rand::rng(), StdRng |
| A repeatable random sequence | StdRng::seed_from_u64(42) |
| Random unique IDs | uuid::Uuid::new_v4() (the uuid crate) |
cargo add chrono --features serde
cargo add regex randPart 1: Time in the standard library
1. Measuring elapsed time
Instant is a monotonic clock: it only moves forward, so it's the right tool for measuring how long something takes. Subtracting two instants, or calling elapsed(), gives a Duration, whose Debug format is human-readable.
use std::thread;
use std::time::{Duration, Instant};
fn main() {
let start = Instant::now();
thread::sleep(Duration::from_millis(50));
let elapsed = start.elapsed();
println!("took at least 50 ms: {}", elapsed >= Duration::from_millis(50));
let timeout = Duration::from_secs(90) + Duration::from_millis(250);
println!("{timeout:?} = {} s = {} ms", timeout.as_secs_f64(), timeout.as_millis());
println!("{:?} {:?} {:?}", Duration::from_micros(1_500), Duration::from_secs(3_600), timeout * 2);
}took at least 50 ms: true
90.25s = 90.25 s = 90250 ms
1.5ms 3600s 180.5s2. The wall clock
SystemTime is the calendar clock, which can jump when the system time is corrected. Its only link to dates is the Unix epoch, so for anything involving days, months, or time zones you need a date crate.
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let since_epoch = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock is after 1970");
println!("seconds since 1970 fit in an i64: {}", since_epoch.as_secs() < i64::MAX as u64);
println!("after September 2023: {}", since_epoch.as_secs() > 1_695_000_000);
}seconds since 1970 fit in an i64: true
after September 2023: truePart 2: Dates and times with chrono
3. Calendar dates
NaiveDate is a date without a time zone. Constructors return Option because not every combination is a real date. Adding Months clamps to the end of the month, so January 31 plus one month is the last day of February.
use chrono::{Datelike, Days, Months, NaiveDate};
fn main() {
let release = NaiveDate::from_ymd_opt(2026, 9, 27).unwrap();
println!("{release} is a {}, day {} of the year", release.weekday(), release.ordinal());
println!("February 30th: {:?}", NaiveDate::from_ymd_opt(2026, 2, 30));
println!("+10 days: {}", release + Days::new(10));
let jan31 = NaiveDate::from_ymd_opt(2026, 1, 31).unwrap();
println!("Jan 31 + 1 month: {:?}", jan31.checked_add_months(Months::new(1)));
let deadline = NaiveDate::from_ymd_opt(2026, 12, 24).unwrap();
println!("days until the deadline: {}", (deadline - release).num_days());
let week: Vec<String> = release.iter_days().take(3).map(|d| d.format("%a %d").to_string()).collect();
println!("{week:?}");
println!("2028 is a leap year: {}", NaiveDate::from_ymd_opt(2028, 2, 29).is_some());
}2026-09-27 is a Sun, day 270 of the year
February 30th: None
+10 days: 2026-10-07
Jan 31 + 1 month: Some(2026-02-28)
days until the deadline: 88
["Sun 27", "Mon 28", "Tue 29"]
2028 is a leap year: true4. Formatting and parsing
chrono uses strftime-style format strings: %Y year, %m month, %d day, %H:%M:%S time, %A weekday name, %B month name. parse_from_str uses the same codes in reverse. ISO 8601 dates parse directly with .parse().
use chrono::NaiveDate;
fn main() {
let d = NaiveDate::from_ymd_opt(2026, 9, 7).unwrap();
println!("{}", d.format("%Y-%m-%d"));
println!("{}", d.format("%d/%m/%Y"));
println!("{}", d.format("%A, %-d %B %Y")); // %-d: day without a leading zero
println!("{}", d.format("week %V of %G"));
println!("{:?}", NaiveDate::parse_from_str("27/09/2026", "%d/%m/%Y"));
println!("{:?}", "2026-09-27".parse::<NaiveDate>());
for bad in ["2026-13-01", "27.09.2026", "yesterday"] {
println!("{bad:?}: {}", bad.parse::<NaiveDate>().unwrap_err());
}
}2026-09-07
07/09/2026
Monday, 7 September 2026
week 37 of 2026
Ok(2026-09-27)
Ok(2026-09-27)
"2026-13-01": input is out of range
"27.09.2026": input contains invalid characters
"yesterday": input contains invalid characters| Example | chrono |
|---|---|
2026-09-27 |
%Y-%m-%d |
27/09/2026 14:05 |
%d/%m/%Y %H:%M |
Sunday, September 27 |
%A, %B %-d |
02:05 PM |
%I:%M %p |
5. Points in time: DateTime<Utc>
A DateTime<Utc> is an exact moment. Store and compute in UTC, and convert to a local time only for display. It converts to and from Unix timestamps and RFC 3339 strings, the format of JSON APIs.
use chrono::{DateTime, Datelike, SecondsFormat, TimeZone, Utc};
fn main() {
let launch = Utc.with_ymd_and_hms(2026, 9, 27, 14, 30, 0).unwrap();
println!("{launch}");
println!("{}", launch.to_rfc3339_opts(SecondsFormat::Secs, true));
println!("unix timestamp: {}", launch.timestamp());
let from_ts = DateTime::from_timestamp(1_790_000_000, 0).unwrap();
println!("1790000000 is {from_ts}");
let now = Utc::now(); // the current moment, different on every run
println!("current year is at least 2026: {}", now.year() >= 2026);
}2026-09-27 14:30:00 UTC
2026-09-27T14:30:00Z
unix timestamp: 1790519400
1790000000 is 2026-09-21 14:13:20 UTC
current year is at least 2026: true6. Offsets and parsing timestamps
DateTime<FixedOffset> keeps the offset it was given. Two values with different offsets that describe the same moment compare equal. For named time zones with daylight saving rules (Europe/Lisbon), add the chrono-tz crate.
use chrono::{DateTime, FixedOffset, Utc};
fn main() -> Result<(), chrono::ParseError> {
let berlin: DateTime<FixedOffset> = DateTime::parse_from_rfc3339("2026-09-27T16:30:00+02:00")?;
let utc: DateTime<Utc> = berlin.with_timezone(&Utc);
println!("{berlin} is {utc}");
println!("same moment: {}", berlin == utc);
let india = FixedOffset::east_opt(5 * 3600 + 30 * 60).unwrap();
println!("in India: {}", utc.with_timezone(&india).format("%Y-%m-%d %H:%M %:z"));
println!("{:?}", DateTime::parse_from_rfc3339("2026-09-27 16:30"));
Ok(())
}2026-09-27 16:30:00 +02:00 is 2026-09-27 14:30:00 UTC
same moment: true
in India: 2026-09-27 20:00 +05:30
Err(ParseError(TooShort))7. Durations between moments
Subtracting two DateTimes gives a TimeDelta (also available as chrono::Duration), which can be negative, unlike std::time::Duration. It converts to whole days, hours, minutes, or seconds.
use chrono::{DateTime, TimeDelta, TimeZone, Utc};
fn remaining(now: DateTime<Utc>, deadline: DateTime<Utc>) -> String {
let left = deadline - now;
if left < TimeDelta::zero() {
return format!("overdue by {} hours", -left.num_hours());
}
format!("{} days, {} hours left", left.num_days(), left.num_hours() % 24)
}
fn main() {
let now = Utc.with_ymd_and_hms(2026, 9, 27, 9, 0, 0).unwrap();
let deadline = Utc.with_ymd_and_hms(2026, 9, 30, 17, 0, 0).unwrap();
println!("{}", remaining(now, deadline));
println!("{}", remaining(now + TimeDelta::days(4), deadline));
let lease = now + TimeDelta::minutes(90);
println!("lease ends {}", lease.format("%H:%M"));
println!("{:?}", TimeDelta::try_days(i64::MAX)); // out of range: None, instead of a panic
}3 days, 8 hours left
overdue by 16 hours
lease ends 10:30
None8. Dates in JSON
With chrono's serde feature, DateTime<Utc> serializes as an RFC 3339 string and NaiveDate as YYYY-MM-DD. For APIs that send Unix timestamps, chrono::serde::ts_seconds changes one field's format (Serialization with serde explains with).
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Booking {
created_at: DateTime<Utc>,
check_in: NaiveDate,
#[serde(with = "chrono::serde::ts_seconds")]
expires: DateTime<Utc>,
}
fn main() -> Result<(), serde_json::Error> {
let booking = Booking {
created_at: Utc.with_ymd_and_hms(2026, 9, 27, 14, 30, 5).unwrap(),
check_in: NaiveDate::from_ymd_opt(2026, 10, 3).unwrap(),
expires: Utc.with_ymd_and_hms(2026, 9, 28, 0, 0, 0).unwrap(),
};
let json = serde_json::to_string(&booking)?;
println!("{json}");
let parsed: Booking = serde_json::from_str(r#"{"created_at":"2026-09-27T16:30:05+02:00","check_in":"2026-10-03","expires":1790553600}"#)?;
println!("{parsed:?}");
Ok(())
}{"created_at":"2026-09-27T14:30:05Z","check_in":"2026-10-03","expires":1790553600}
Booking { created_at: 2026-09-27T14:30:05Z, check_in: 2026-10-03, expires: 2026-09-28T00:00:00Z }9. Realistic example: billing dates
Monthly billing on a customer's signup day: the 31st becomes the last day of shorter months, and a bill that falls on a weekend moves to the next Monday.
use chrono::{Datelike, Days, Months, NaiveDate, Weekday};
fn next_business_day(d: NaiveDate) -> NaiveDate {
match d.weekday() {
Weekday::Sat => d + Days::new(2),
Weekday::Sun => d + Days::new(1),
_ => d,
}
}
fn billing_dates(signup: NaiveDate, count: u32) -> Vec<NaiveDate> {
// Always count from the signup date, so the 31st isn't lost after February.
(1..=count).filter_map(|n| signup.checked_add_months(Months::new(n))).map(next_business_day).collect()
}
fn main() {
let signup = NaiveDate::from_ymd_opt(2026, 1, 31).unwrap();
for date in billing_dates(signup, 6) {
println!("{}", date.format("%a %Y-%m-%d"));
}
}Mon 2026-03-02
Tue 2026-03-31
Thu 2026-04-30
Mon 2026-06-01
Tue 2026-06-30
Fri 2026-07-31Part 3: Regular expressions with regex
10. Matching and finding
Regex::new compiles a pattern and returns a Result, since the pattern can be invalid. is_match checks, find returns the first match with its position, and find_iter returns all of them. Raw strings (r"...") avoid doubling every backslash.
use regex::Regex;
fn main() -> Result<(), regex::Error> {
let order_id = Regex::new(r"\bORD-\d{4,}\b")?;
let text = "Refund ORD-20931 and ORD-551, then check ORD-77120.";
println!("contains an order id: {}", order_id.is_match(text));
if let Some(m) = order_id.find(text) {
println!("first: {} at {}..{}", m.as_str(), m.start(), m.end());
}
let all: Vec<&str> = order_id.find_iter(text).map(|m| m.as_str()).collect();
println!("all: {all:?}");
println!("{}", Regex::new(r"ORD-(\d+").unwrap_err());
Ok(())
}contains an order id: true
first: ORD-20931 at 7..16
all: ["ORD-20931", "ORD-77120"]
regex parse error:
ORD-(\d+
^
error: unclosed groupORD-551 has only three digits, so \d{4,} skips it.
11. Capture groups
Parentheses capture parts of a match. Named groups, (?<name>...), make the code readable: caps["name"] returns the text, and caps.name("name") returns an Option for groups that may not participate.
use regex::Regex;
fn main() {
let line = Regex::new(r#"^(?<ip>\S+) \S+ \S+ \[(?<time>[^\]]+)\] "(?<method>[A-Z]+) (?<path>\S+)[^"]*" (?<status>\d{3}) (?<bytes>\d+|-)"#).unwrap();
let log = [
r#"203.0.113.9 - - [27/Sep/2026:10:15:02 +0000] "GET /api/orders?page=2 HTTP/1.1" 200 5123"#,
r#"198.51.100.4 - ana [27/Sep/2026:10:15:07 +0000] "POST /api/orders HTTP/1.1" 201 -"#,
"garbage line",
];
for entry in log {
match line.captures(entry) {
Some(caps) => {
let bytes: u64 = caps["bytes"].parse().unwrap_or(0);
println!("{} {} {} -> {} ({bytes} bytes) from {}", &caps["time"][12..20], &caps["method"], &caps["path"], &caps["status"], &caps["ip"]);
}
None => println!("no match: {entry:?}"),
}
}
}10:15:02 GET /api/orders?page=2 -> 200 (5123 bytes) from 203.0.113.9
10:15:07 POST /api/orders -> 201 (0 bytes) from 198.51.100.4
no match: "garbage line"12. Replacing
replace_all substitutes every match. The replacement can refer to groups with $1 or ${name}, or be a closure that computes each replacement. It returns a Cow<str>, which borrows the input when nothing matched.
use regex::{Captures, Regex};
fn main() {
let dates = Regex::new(r"(?<d>\d{2})/(?<m>\d{2})/(?<y>\d{4})").unwrap();
println!("{}", dates.replace_all("due 27/09/2026, paid 03/10/2026", "${y}-${m}-${d}"));
// Mask all but the last four digits of card numbers.
let card = Regex::new(r"\b(?:\d[ -]?){12}(\d{4})\b").unwrap();
println!("{}", card.replace_all("card 4242 4242 4242 4242 charged", "**** **** **** $1"));
// A closure computes each replacement.
let price = Regex::new(r"\$(\d+)\.(\d{2})").unwrap();
let in_cents = price.replace_all("coffee $3.50, cake $4.25", |caps: &Captures| {
let cents = caps[1].parse::<u32>().unwrap() * 100 + caps[2].parse::<u32>().unwrap();
format!("{cents}c")
});
println!("{in_cents}");
}due 2026-09-27, paid 2026-10-03
card **** **** **** 4242 charged
coffee 350c, cake 425c13. Compile once, use many times
Compiling a pattern is much slower than using it. Don't call Regex::new inside a loop or a frequently called function: compile it once into a static LazyLock<Regex>, which is created on first use and then shared, also across threads.
use regex::Regex;
use std::sync::LazyLock;
static SLUG_UNSAFE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9]+").unwrap());
fn slugify(title: &str) -> String {
SLUG_UNSAFE.replace_all(&title.to_lowercase(), "-").trim_matches('-').to_string()
}
fn main() {
for title in ["Rust in Production: Part 1", " Async & Await! ", "Ünïcödé Tïtlé"] {
println!("{:<28} -> {}", format!("{title:?}"), slugify(title));
}
}"Rust in Production: Part 1" -> rust-in-production-part-1
" Async & Await! " -> async-await
"Ünïcödé Tïtlé" -> n-c-d-t-tlThe last title shows that [^a-z0-9] treats letters like Ü as "unsafe" and replaces them. Real slug libraries transliterate them first.
14. What Rust's regex engine guarantees
Backtracking regex engines, used by many languages, can take exponential time on a pattern like (a+)+$ with the wrong input, a denial-of-service risk known as ReDoS. Rust's regex crate guarantees matching in linear time in the size of the input. The price is that it doesn't support features that need backtracking: look-around and backreferences.
use regex::Regex;
use std::time::Instant;
fn main() {
let evil = Regex::new(r"^(a+)+$").unwrap();
let input = format!("{}b", "a".repeat(50_000));
let start = Instant::now();
println!("matches: {}", evil.is_match(&input));
println!("finished in under a second: {}", start.elapsed().as_secs() < 1);
for unsupported in [r"foo(?=bar)", r"(\w)\1"] {
let first_line = Regex::new(unsupported).unwrap_err().to_string().lines().last().unwrap_or("").to_string();
println!("{unsupported:<12} {first_line}");
}
}matches: false
finished in under a second: true
foo(?=bar) error: look-around, including look-ahead and look-behind, is not supported
(\w)\1 error: backreferences are not supportedWith a backtracking engine, the same check takes exponentially longer with every extra a: a few dozen of them are enough to hang a thread. When you really need look-around, the fancy-regex crate adds it, at the cost of the guarantee.
15. Splitting, and matching many patterns at once
split splits on every match. RegexSet tests a line against many patterns in a single pass and tells you which ones matched, which is useful for classifying log lines.
use regex::{Regex, RegexSet};
fn main() {
let separators = Regex::new(r"\s*[,;|]\s*").unwrap();
let tags: Vec<&str> = separators.split("rust, tokio ;serde| cli").collect();
println!("{tags:?}");
let categories = ["timeout", "auth", "disk"];
let set = RegexSet::new([r"(?i)timed? ?out", r"(?i)\b(401|403|unauthori[sz]ed)\b", r"(?i)no space|disk full"]).unwrap();
for line in ["upstream timed out after 30s", "HTTP 401 Unauthorized", "write failed: no space left on device", "all good"] {
let hits: Vec<&str> = set.matches(line).into_iter().map(|i| categories[i]).collect();
println!("{line:<40} {hits:?}");
}
}["rust", "tokio", "serde", "cli"]
upstream timed out after 30s ["timeout"]
HTTP 401 Unauthorized ["auth"]
write failed: no space left on device ["disk"]
all good []Part 4: Random numbers with rand
16. Random numbers
rand::rng() returns the thread-local generator, fast and cryptographically secure, seeded from the operating system. The methods come from the RngExt trait: random_range for a number in a range, random for any type, and random_bool for a coin that lands true with a given probability.
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let die: u8 = rng.random_range(1..=6);
let unit: f64 = rng.random(); // in [0, 1)
let heads = rng.random_bool(0.5);
println!("die in 1..=6: {}", (1..=6).contains(&die));
println!("unit in [0, 1): {}", (0.0..1.0).contains(&unit));
println!("coin is a bool: {}", heads || !heads);
let rolls: Vec<u8> = (0..1_000).map(|_| rng.random_range(1..=6)).collect();
let sixes = rolls.iter().filter(|&&r| r == 6).count();
println!("about a sixth are sixes: {}", (100..=240).contains(&sixes));
}die in 1..=6: true
unit in [0, 1): true
coin is a bool: true
about a sixth are sixes: trueThe output only checks properties, because the actual values change on every run.
17. Reproducible randomness with a seed
Simulations, tests, and procedurally generated content often need the same random sequence every time. StdRng::seed_from_u64 gives exactly that: two generators with the same seed produce the same values.
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
fn sample(seed: u64) -> Vec<u32> {
let mut rng = StdRng::seed_from_u64(seed);
(0..5).map(|_| rng.random_range(0..100)).collect()
}
fn main() {
println!("seed 42 twice gives the same numbers: {}", sample(42) == sample(42));
println!("seed 43 gives different ones: {}", sample(42) != sample(43));
println!("seed 42: {:?}", sample(42));
}seed 42 twice gives the same numbers: true
seed 43 gives different ones: true
seed 42: [13, 52, 24, 54, 86]The exact numbers for a seed can change between major versions of rand, so tests should compare runs with each other rather than against hard-coded values from an older version.
18. Shuffling and choosing
The SliceRandom and IndexedRandom traits add random operations to slices: shuffle, choose for one element, sample for several distinct ones, and choose_weighted for choices with different probabilities.
use rand::rngs::StdRng;
use rand::seq::{IndexedRandom, SliceRandom};
use rand::SeedableRng;
fn main() {
let mut rng = StdRng::seed_from_u64(7); // seeded so this page is reproducible
let mut deck: Vec<u32> = (1..=10).collect();
deck.shuffle(&mut rng);
println!("shuffled: {deck:?}");
let servers = ["eu-1", "eu-2", "us-1", "ap-1"];
println!("pick one: {:?}", servers.choose(&mut rng));
let two: Vec<&&str> = servers.sample(&mut rng, 2).collect();
println!("pick two distinct: {two:?}");
let variants = [("control", 80), ("new-checkout", 15), ("one-click", 5)];
let mut counts = [0; 3];
for _ in 0..10_000 {
let chosen = variants.choose_weighted(&mut rng, |v| v.1).unwrap();
counts[variants.iter().position(|v| v.0 == chosen.0).unwrap()] += 1;
}
println!("A/B split over 10000 users: {counts:?}");
println!("empty slice: {:?}", Vec::<u32>::new().choose(&mut rng));
}shuffled: [2, 4, 5, 7, 1, 8, 3, 6, 9, 10]
pick one: Some("eu-1")
pick two distinct: ["eu-1", "eu-2"]
A/B split over 10000 users: [8019, 1479, 502]
empty slice: None19. Random tokens
For session tokens, password-reset links, and API keys, the randomness must be unpredictable, which rand::rng() is. Sampling the Alphanumeric distribution produces random letters and digits.
use rand::distr::Alphanumeric;
use rand::RngExt;
fn token(len: usize) -> String {
rand::rng().sample_iter(Alphanumeric).take(len).map(char::from).collect()
}
fn main() {
let a = token(32);
let b = token(32);
println!("length {}, alphanumeric: {}", a.len(), a.chars().all(|c| c.is_ascii_alphanumeric()));
println!("two tokens differ: {}", a != b);
}length 32, alphanumeric: true
two tokens differ: trueFor unique identifiers rather than secrets, the uuid crate's Uuid::new_v4() produces a random UUID, and Uuid::now_v7() one that also sorts by creation time, which databases index well.
Part 5: Putting them together
20. Realistic example: analyzing a request log
A log analyzer that uses all three: regex parses each line, chrono parses the timestamps and groups requests by hour, and a seeded rand generator produces the sample log.
use chrono::{DateTime, Timelike, Utc};
use rand::rngs::StdRng;
use rand::seq::IndexedRandom;
use rand::{RngExt, SeedableRng};
use regex::Regex;
use std::collections::BTreeMap;
use std::sync::LazyLock;
static LINE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?<ts>\S+) (?<method>GET|POST|PUT|DELETE) (?<path>/\S*) (?<status>\d{3}) (?<ms>\d+)ms$").unwrap());
fn fake_log(lines: usize) -> String {
let mut rng = StdRng::seed_from_u64(2026);
let paths = ["/api/orders", "/api/users", "/health", "/api/search"];
let start = DateTime::parse_from_rfc3339("2026-09-27T08:00:00Z").unwrap().with_timezone(&Utc);
let mut out = String::new();
for i in 0..lines {
let at = start + chrono::TimeDelta::seconds(i as i64 * 97);
let status = if rng.random_bool(0.05) { 500 } else { 200 };
let ms = rng.random_range(5..400);
out.push_str(&format!("{} GET {} {status} {ms}ms\n", at.to_rfc3339(), paths.choose(&mut rng).unwrap()));
}
out.push_str("not a log line\n");
out
}
fn main() {
let log = fake_log(200);
let (mut per_hour, mut slowest, mut errors, mut skipped) = (BTreeMap::new(), BTreeMap::new(), 0, 0);
for line in log.lines() {
let Some(caps) = LINE.captures(line) else {
skipped += 1;
continue;
};
let Ok(ts) = DateTime::parse_from_rfc3339(&caps["ts"]) else {
skipped += 1;
continue;
};
*per_hour.entry(ts.hour()).or_insert(0) += 1;
let ms: u32 = caps["ms"].parse().unwrap();
let worst = slowest.entry(caps["path"].to_string()).or_insert(0);
*worst = ms.max(*worst);
if &caps["status"] == "500" {
errors += 1;
}
}
println!("requests per hour: {per_hour:?}");
println!("slowest per path (ms): {slowest:?}");
println!("errors: {errors}, unparsable lines: {skipped}");
}requests per hour: {8: 38, 9: 37, 10: 37, 11: 37, 12: 37, 13: 14}
slowest per path (ms): {"/api/orders": 377, "/api/search": 388, "/api/users": 395, "/health": 396}
errors: 7, unparsable lines: 1Summary
std::timehasInstantfor measuring,Durationfor spans, andSystemTimefor the wall clock, but no calendar.- chrono:
NaiveDateandNaiveDateTimefor local dates,DateTime<Utc>for moments,FixedOffsetfor offsets,TimeDeltafor differences. Constructors returnOption,Monthsclamps to the end of the month, andformat/parse_from_struse%Y-%m-%d-style codes. Addchrono-tzfor named time zones. - regex: compile once (
LazyLock), thenis_match,find_iter,captureswith named groups,replace_all,split, andRegexSet. Matching is linear-time, with no look-around or backreferences. - rand:
rand::rng()withrandom_range,random,random_bool;shuffle,choose,sample,choose_weightedon slices;StdRng::seed_from_u64for reproducible sequences.
Next: Logging & Tracing.
