Guide contents

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)
Terminal
cargo add chrono --features serde
cargo add regex rand

Part 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.

Rust
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);
}
Output
took at least 50 ms: true
90.25s = 90.25 s = 90250 ms
1.5ms 3600s 180.5s

2. 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.

Rust
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);
}
Output
seconds since 1970 fit in an i64: true
after September 2023: true

Part 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.

Rust
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());
}
Output
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: true

4. 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().

Rust
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());
    }
}
Output
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.

Rust
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);
}
Output
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: true

6. 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.

Rust
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(())
}
Output
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.

Rust
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
}
Output
3 days, 8 hours left
overdue by 16 hours
lease ends 10:30
None

8. 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).

Rust
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(())
}
Output
{"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.

Rust
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"));
    }
}
Output
Mon 2026-03-02
Tue 2026-03-31
Thu 2026-04-30
Mon 2026-06-01
Tue 2026-06-30
Fri 2026-07-31

Part 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.

Rust
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(())
}
Output
contains an order id: true
first: ORD-20931 at 7..16
all: ["ORD-20931", "ORD-77120"]
regex parse error:
    ORD-(\d+
        ^
error: unclosed group

ORD-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.

Rust
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:?}"),
        }
    }
}
Output
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.

Rust
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}");
}
Output
due 2026-09-27, paid 2026-10-03
card **** **** **** 4242 charged
coffee 350c, cake 425c

13. 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.

Rust
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));
    }
}
Output
"Rust in Production: Part 1" -> rust-in-production-part-1
"  Async & Await!  "         -> async-await
"Ünïcödé Tïtlé"              -> n-c-d-t-tl

The 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.

Rust
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}");
    }
}
Output
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 supported

With 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.

Rust
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:?}");
    }
}
Output
["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.

Rust
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));
}
Output
die in 1..=6: true
unit in [0, 1): true
coin is a bool: true
about a sixth are sixes: true

The 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.

Rust
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));
}
Output
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.

Rust
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));
}
Output
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: None

19. 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.

Rust
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);
}
Output
length 32, alphanumeric: true
two tokens differ: true

For 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.

Rust
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}");
}
Output
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: 1

Summary

  • std::time has Instant for measuring, Duration for spans, and SystemTime for the wall clock, but no calendar.
  • chrono: NaiveDate and NaiveDateTime for local dates, DateTime<Utc> for moments, FixedOffset for offsets, TimeDelta for differences. Constructors return Option, Months clamps to the end of the month, and format/parse_from_str use %Y-%m-%d-style codes. Add chrono-tz for named time zones.
  • regex: compile once (LazyLock), then is_match, find_iter, captures with named groups, replace_all, split, and RegexSet. Matching is linear-time, with no look-around or backreferences.
  • rand: rand::rng() with random_range, random, random_bool; shuffle, choose, sample, choose_weighted on slices; StdRng::seed_from_u64 for reproducible sequences.

Next: Logging & Tracing.